Overhaul ansible-test SSH key management. (#73451)
* Pass remote.sh to shell over stdin. * Pass docker.sh to shell over stdin. * Standardize SSH key management. * Update docker containers.
This commit is contained in:
parent
218f5c3648
commit
a9b5bebab3
9 changed files with 137 additions and 48 deletions
2
changelogs/fragments/ansible-test-ssh-key-management.yml
Normal file
2
changelogs/fragments/ansible-test-ssh-key-management.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
bugfixes:
|
||||||
|
- ansible-test - Unified SSH key management for all instances created with the ``--remote`` or ``--docker`` options.
|
|
@ -1,15 +1,15 @@
|
||||||
default name=quay.io/ansible/default-test-container:2.11.0 python=3.6,2.6,2.7,3.5,3.7,3.8,3.9 seccomp=unconfined context=collection
|
default name=quay.io/ansible/default-test-container:3.0.0 python=3.6,2.6,2.7,3.5,3.7,3.8,3.9 seccomp=unconfined context=collection
|
||||||
default name=quay.io/ansible/ansible-core-test-container:1.9.0 python=3.6,2.6,2.7,3.5,3.7,3.8,3.9 seccomp=unconfined context=ansible-core
|
default name=quay.io/ansible/ansible-core-test-container:3.0.0 python=3.6,2.6,2.7,3.5,3.7,3.8,3.9 seccomp=unconfined context=ansible-core
|
||||||
alpine3 name=quay.io/ansible/alpine3-test-container:1.29.0 python=3.8
|
alpine3 name=quay.io/ansible/alpine3-test-container:2.0.1 python=3.8
|
||||||
centos6 name=quay.io/ansible/centos6-test-container:1.30.0 python=2.6 seccomp=unconfined
|
centos6 name=quay.io/ansible/centos6-test-container:2.0.1 python=2.6 seccomp=unconfined
|
||||||
centos7 name=quay.io/ansible/centos7-test-container:1.29.0 python=2.7 seccomp=unconfined
|
centos7 name=quay.io/ansible/centos7-test-container:2.0.1 python=2.7 seccomp=unconfined
|
||||||
centos8 name=quay.io/ansible/centos8-test-container:1.29.0 python=3.6 seccomp=unconfined
|
centos8 name=quay.io/ansible/centos8-test-container:2.0.1 python=3.6 seccomp=unconfined
|
||||||
fedora30 name=quay.io/ansible/fedora30-test-container:1.17.0 python=3.7
|
fedora30 name=quay.io/ansible/fedora30-test-container:2.0.1 python=3.7
|
||||||
fedora31 name=quay.io/ansible/fedora31-test-container:1.17.0 python=3.7
|
fedora31 name=quay.io/ansible/fedora31-test-container:2.0.1 python=3.7
|
||||||
fedora32 name=quay.io/ansible/fedora32-test-container:1.29.0 python=3.8
|
fedora32 name=quay.io/ansible/fedora32-test-container:2.0.1 python=3.8
|
||||||
fedora33 name=quay.io/ansible/fedora33-test-container:1.29.0 python=3.9
|
fedora33 name=quay.io/ansible/fedora33-test-container:2.0.1 python=3.9
|
||||||
opensuse15py2 name=quay.io/ansible/opensuse15py2-test-container:1.29.0 python=2.7
|
opensuse15py2 name=quay.io/ansible/opensuse15py2-test-container:2.0.1 python=2.7
|
||||||
opensuse15 name=quay.io/ansible/opensuse15-test-container:1.29.0 python=3.6
|
opensuse15 name=quay.io/ansible/opensuse15-test-container:2.0.1 python=3.6
|
||||||
ubuntu1604 name=quay.io/ansible/ubuntu1604-test-container:1.29.0 python=2.7 seccomp=unconfined
|
ubuntu1604 name=quay.io/ansible/ubuntu1604-test-container:2.0.1 python=2.7 seccomp=unconfined
|
||||||
ubuntu1804 name=quay.io/ansible/ubuntu1804-test-container:1.29.0 python=3.6 seccomp=unconfined
|
ubuntu1804 name=quay.io/ansible/ubuntu1804-test-container:2.0.1 python=3.6 seccomp=unconfined
|
||||||
ubuntu2004 name=quay.io/ansible/ubuntu2004-test-container:1.29.0 python=3.8 seccomp=unconfined
|
ubuntu2004 name=quay.io/ansible/ubuntu2004-test-container:2.0.1 python=3.8 seccomp=unconfined
|
||||||
|
|
|
@ -2,9 +2,10 @@
|
||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
platform="$1"
|
platform=#{platform}
|
||||||
platform_version="$2"
|
platform_version=#{platform_version}
|
||||||
python_version="$3"
|
python_version=#{python_version}
|
||||||
|
|
||||||
python_interpreter="python${python_version}"
|
python_interpreter="python${python_version}"
|
||||||
|
|
||||||
cd ~/
|
cd ~/
|
||||||
|
@ -164,24 +165,6 @@ elif [ "${platform}" = "aix" ]; then
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Generate our ssh key and add it to our authorized_keys file.
|
|
||||||
# We also need to add localhost's server keys to known_hosts.
|
|
||||||
|
|
||||||
if [ ! -f "${HOME}/.ssh/id_rsa.pub" ]; then
|
|
||||||
ssh-keygen -m PEM -q -t rsa -N '' -f "${HOME}/.ssh/id_rsa"
|
|
||||||
# newer ssh-keygen PEM output (such as on RHEL 8.1) is not recognized by paramiko
|
|
||||||
touch "${HOME}/.ssh/id_rsa.new"
|
|
||||||
chmod 0600 "${HOME}/.ssh/id_rsa.new"
|
|
||||||
sed 's/\(BEGIN\|END\) PRIVATE KEY/\1 RSA PRIVATE KEY/' "${HOME}/.ssh/id_rsa" > "${HOME}/.ssh/id_rsa.new"
|
|
||||||
mv "${HOME}/.ssh/id_rsa.new" "${HOME}/.ssh/id_rsa"
|
|
||||||
cat "${HOME}/.ssh/id_rsa.pub" >> "${HOME}/.ssh/authorized_keys"
|
|
||||||
chmod 0600 "${HOME}/.ssh/authorized_keys"
|
|
||||||
for key in /etc/ssh/ssh_host_*_key.pub; do
|
|
||||||
pk=$(cat "${key}")
|
|
||||||
echo "localhost ${pk}" >> "${HOME}/.ssh/known_hosts"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Improve prompts on remote host for interactive use.
|
# Improve prompts on remote host for interactive use.
|
||||||
# shellcheck disable=SC1117
|
# shellcheck disable=SC1117
|
||||||
cat << EOF > ~/.bashrc
|
cat << EOF > ~/.bashrc
|
||||||
|
|
35
test/lib/ansible_test/_data/setup/ssh-keys.sh
Normal file
35
test/lib/ansible_test/_data/setup/ssh-keys.sh
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Configure SSH keys.
|
||||||
|
|
||||||
|
ssh_public_key=#{ssh_public_key}
|
||||||
|
ssh_private_key=#{ssh_private_key}
|
||||||
|
ssh_key_type=#{ssh_key_type}
|
||||||
|
|
||||||
|
ssh_path="${HOME}/.ssh"
|
||||||
|
private_key_path="${ssh_path}/id_${ssh_key_type}"
|
||||||
|
|
||||||
|
if [ ! -f "${private_key_path}" ]; then
|
||||||
|
# write public/private ssh key pair
|
||||||
|
public_key_path="${private_key_path}.pub"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2174
|
||||||
|
mkdir -m 0700 -p "${ssh_path}"
|
||||||
|
touch "${public_key_path}" "${private_key_path}"
|
||||||
|
chmod 0600 "${public_key_path}" "${private_key_path}"
|
||||||
|
echo "${ssh_public_key}" > "${public_key_path}"
|
||||||
|
echo "${ssh_private_key}" > "${private_key_path}"
|
||||||
|
|
||||||
|
# add public key to authorized_keys
|
||||||
|
authoried_keys_path="${HOME}/.ssh/authorized_keys"
|
||||||
|
|
||||||
|
# the existing file is overwritten to avoid conflicts (ex: RHEL on EC2 blocks root login)
|
||||||
|
cat "${public_key_path}" > "${authoried_keys_path}"
|
||||||
|
chmod 0600 "${authoried_keys_path}"
|
||||||
|
|
||||||
|
# add localhost's server keys to known_hosts
|
||||||
|
known_hosts_path="${HOME}/.ssh/known_hosts"
|
||||||
|
|
||||||
|
for key in /etc/ssh/ssh_host_*_key.pub; do
|
||||||
|
echo "localhost $(cat "${key}")" >> "${known_hosts_path}"
|
||||||
|
done
|
||||||
|
fi
|
|
@ -490,8 +490,9 @@ class CoreHttpError(HttpError):
|
||||||
|
|
||||||
class SshKey:
|
class SshKey:
|
||||||
"""Container for SSH key used to connect to remote instances."""
|
"""Container for SSH key used to connect to remote instances."""
|
||||||
KEY_NAME = 'id_rsa'
|
KEY_TYPE = 'rsa' # RSA is used to maintain compatibility with paramiko and EC2
|
||||||
PUB_NAME = 'id_rsa.pub'
|
KEY_NAME = 'id_%s' % KEY_TYPE
|
||||||
|
PUB_NAME = '%s.pub' % KEY_NAME
|
||||||
|
|
||||||
def __init__(self, args):
|
def __init__(self, args):
|
||||||
"""
|
"""
|
||||||
|
@ -519,8 +520,10 @@ class SshKey:
|
||||||
|
|
||||||
if args.explain:
|
if args.explain:
|
||||||
self.pub_contents = None
|
self.pub_contents = None
|
||||||
|
self.key_contents = None
|
||||||
else:
|
else:
|
||||||
self.pub_contents = read_text_file(self.pub).strip()
|
self.pub_contents = read_text_file(self.pub).strip()
|
||||||
|
self.key_contents = read_text_file(self.key).strip()
|
||||||
|
|
||||||
def get_in_tree_key_pair_paths(self): # type: () -> t.Optional[t.Tuple[str, str]]
|
def get_in_tree_key_pair_paths(self): # type: () -> t.Optional[t.Tuple[str, str]]
|
||||||
"""Return the ansible-test SSH key pair paths from the content tree."""
|
"""Return the ansible-test SSH key pair paths from the content tree."""
|
||||||
|
@ -562,7 +565,7 @@ class SshKey:
|
||||||
make_dirs(os.path.dirname(key))
|
make_dirs(os.path.dirname(key))
|
||||||
|
|
||||||
if not os.path.isfile(key) or not os.path.isfile(pub):
|
if not os.path.isfile(key) or not os.path.isfile(pub):
|
||||||
run_command(args, ['ssh-keygen', '-m', 'PEM', '-q', '-t', 'rsa', '-N', '', '-f', key])
|
run_command(args, ['ssh-keygen', '-m', 'PEM', '-q', '-t', self.KEY_TYPE, '-N', '', '-f', key])
|
||||||
|
|
||||||
# newer ssh-keygen PEM output (such as on RHEL 8.1) is not recognized by paramiko
|
# newer ssh-keygen PEM output (such as on RHEL 8.1) is not recognized by paramiko
|
||||||
key_contents = read_text_file(key)
|
key_contents = read_text_file(key)
|
||||||
|
|
|
@ -11,6 +11,7 @@ from . import types as t
|
||||||
|
|
||||||
from .io import (
|
from .io import (
|
||||||
make_dirs,
|
make_dirs,
|
||||||
|
read_text_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .executor import (
|
from .executor import (
|
||||||
|
@ -36,11 +37,13 @@ from .config import (
|
||||||
|
|
||||||
from .core_ci import (
|
from .core_ci import (
|
||||||
AnsibleCoreCI,
|
AnsibleCoreCI,
|
||||||
|
SshKey,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .manage_ci import (
|
from .manage_ci import (
|
||||||
ManagePosixCI,
|
ManagePosixCI,
|
||||||
ManageWindowsCI,
|
ManageWindowsCI,
|
||||||
|
get_ssh_key_setup,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .util import (
|
from .util import (
|
||||||
|
@ -334,9 +337,16 @@ def delegate_docker(args, exclude, require, integration_targets):
|
||||||
else:
|
else:
|
||||||
test_id = test_id.strip()
|
test_id = test_id.strip()
|
||||||
|
|
||||||
|
setup_sh = read_text_file(os.path.join(ANSIBLE_TEST_DATA_ROOT, 'setup', 'docker.sh'))
|
||||||
|
|
||||||
|
ssh_keys_sh = get_ssh_key_setup(SshKey(args))
|
||||||
|
|
||||||
|
setup_sh += ssh_keys_sh
|
||||||
|
shell = setup_sh.splitlines()[0][2:]
|
||||||
|
|
||||||
|
docker_exec(args, test_id, [shell], data=setup_sh)
|
||||||
|
|
||||||
# write temporary files to /root since /tmp isn't ready immediately on container start
|
# write temporary files to /root since /tmp isn't ready immediately on container start
|
||||||
docker_put(args, test_id, os.path.join(ANSIBLE_TEST_DATA_ROOT, 'setup', 'docker.sh'), '/root/docker.sh')
|
|
||||||
docker_exec(args, test_id, ['/bin/bash', '/root/docker.sh'])
|
|
||||||
docker_put(args, test_id, local_source_fd.name, '/root/test.tgz')
|
docker_put(args, test_id, local_source_fd.name, '/root/test.tgz')
|
||||||
docker_exec(args, test_id, ['tar', 'oxzf', '/root/test.tgz', '-C', '/root'])
|
docker_exec(args, test_id, ['tar', 'oxzf', '/root/test.tgz', '-C', '/root'])
|
||||||
|
|
||||||
|
|
|
@ -377,7 +377,7 @@ def docker_network_inspect(args, network):
|
||||||
raise ex
|
raise ex
|
||||||
|
|
||||||
|
|
||||||
def docker_exec(args, container_id, cmd, options=None, capture=False, stdin=None, stdout=None):
|
def docker_exec(args, container_id, cmd, options=None, capture=False, stdin=None, stdout=None, data=None):
|
||||||
"""
|
"""
|
||||||
:type args: EnvironmentConfig
|
:type args: EnvironmentConfig
|
||||||
:type container_id: str
|
:type container_id: str
|
||||||
|
@ -386,12 +386,16 @@ def docker_exec(args, container_id, cmd, options=None, capture=False, stdin=None
|
||||||
:type capture: bool
|
:type capture: bool
|
||||||
:type stdin: BinaryIO | None
|
:type stdin: BinaryIO | None
|
||||||
:type stdout: BinaryIO | None
|
:type stdout: BinaryIO | None
|
||||||
|
:type data: str | None
|
||||||
:rtype: str | None, str | None
|
:rtype: str | None, str | None
|
||||||
"""
|
"""
|
||||||
if not options:
|
if not options:
|
||||||
options = []
|
options = []
|
||||||
|
|
||||||
return docker_command(args, ['exec'] + options + [container_id] + cmd, capture=capture, stdin=stdin, stdout=stdout)
|
if data:
|
||||||
|
options.append('-i')
|
||||||
|
|
||||||
|
return docker_command(args, ['exec'] + options + [container_id] + cmd, capture=capture, stdin=stdin, stdout=stdout, data=data)
|
||||||
|
|
||||||
|
|
||||||
def docker_info(args):
|
def docker_info(args):
|
||||||
|
|
|
@ -8,6 +8,10 @@ import time
|
||||||
|
|
||||||
from . import types as t
|
from . import types as t
|
||||||
|
|
||||||
|
from .io import (
|
||||||
|
read_text_file,
|
||||||
|
)
|
||||||
|
|
||||||
from .util import (
|
from .util import (
|
||||||
SubprocessError,
|
SubprocessError,
|
||||||
ApplicationError,
|
ApplicationError,
|
||||||
|
@ -20,10 +24,12 @@ from .util_common import (
|
||||||
intercept_command,
|
intercept_command,
|
||||||
get_network_completion,
|
get_network_completion,
|
||||||
run_command,
|
run_command,
|
||||||
|
ShellScriptTemplate,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .core_ci import (
|
from .core_ci import (
|
||||||
AnsibleCoreCI,
|
AnsibleCoreCI,
|
||||||
|
SshKey,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .ansible_util import (
|
from .ansible_util import (
|
||||||
|
@ -268,8 +274,19 @@ class ManagePosixCI:
|
||||||
"""Configure remote host for testing.
|
"""Configure remote host for testing.
|
||||||
:type python_version: str
|
:type python_version: str
|
||||||
"""
|
"""
|
||||||
self.upload(os.path.join(ANSIBLE_TEST_DATA_ROOT, 'setup', 'remote.sh'), '/tmp')
|
template = ShellScriptTemplate(read_text_file(os.path.join(ANSIBLE_TEST_DATA_ROOT, 'setup', 'remote.sh')))
|
||||||
self.ssh('chmod +x /tmp/remote.sh && /tmp/remote.sh %s %s %s' % (self.core_ci.platform, self.core_ci.version, python_version))
|
setup_sh = template.substitute(
|
||||||
|
platform=self.core_ci.platform,
|
||||||
|
platform_version=self.core_ci.version,
|
||||||
|
python_version=python_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
ssh_keys_sh = get_ssh_key_setup(self.core_ci.ssh_key)
|
||||||
|
|
||||||
|
setup_sh += ssh_keys_sh
|
||||||
|
shell = setup_sh.splitlines()[0][2:]
|
||||||
|
|
||||||
|
self.ssh(shell, data=setup_sh)
|
||||||
|
|
||||||
def upload_source(self):
|
def upload_source(self):
|
||||||
"""Upload and extract source."""
|
"""Upload and extract source."""
|
||||||
|
@ -302,11 +319,12 @@ class ManagePosixCI:
|
||||||
"""
|
"""
|
||||||
self.scp(local, '%s@%s:%s' % (self.core_ci.connection.username, self.core_ci.connection.hostname, remote))
|
self.scp(local, '%s@%s:%s' % (self.core_ci.connection.username, self.core_ci.connection.hostname, remote))
|
||||||
|
|
||||||
def ssh(self, command, options=None, capture=False):
|
def ssh(self, command, options=None, capture=False, data=None):
|
||||||
"""
|
"""
|
||||||
:type command: str | list[str]
|
:type command: str | list[str]
|
||||||
:type options: list[str] | None
|
:type options: list[str] | None
|
||||||
:type capture: bool
|
:type capture: bool
|
||||||
|
:type data: str | None
|
||||||
:rtype: str | None, str | None
|
:rtype: str | None, str | None
|
||||||
"""
|
"""
|
||||||
if not options:
|
if not options:
|
||||||
|
@ -316,12 +334,18 @@ class ManagePosixCI:
|
||||||
command = ' '.join(cmd_quote(c) for c in command)
|
command = ' '.join(cmd_quote(c) for c in command)
|
||||||
|
|
||||||
command = cmd_quote(command) if self.become else command
|
command = cmd_quote(command) if self.become else command
|
||||||
|
|
||||||
|
options.append('-q')
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
options.append('-tt')
|
||||||
|
|
||||||
return run_command(self.core_ci.args,
|
return run_command(self.core_ci.args,
|
||||||
['ssh', '-tt', '-q'] + self.ssh_args +
|
['ssh'] + self.ssh_args +
|
||||||
options +
|
options +
|
||||||
['-p', str(self.core_ci.connection.port),
|
['-p', str(self.core_ci.connection.port),
|
||||||
'%s@%s' % (self.core_ci.connection.username, self.core_ci.connection.hostname)] +
|
'%s@%s' % (self.core_ci.connection.username, self.core_ci.connection.hostname)] +
|
||||||
self.become + [command], capture=capture)
|
self.become + [command], capture=capture, data=data)
|
||||||
|
|
||||||
def scp(self, src, dst):
|
def scp(self, src, dst):
|
||||||
"""
|
"""
|
||||||
|
@ -340,6 +364,19 @@ class ManagePosixCI:
|
||||||
raise ApplicationError('Failed transfer: %s -> %s' % (src, dst))
|
raise ApplicationError('Failed transfer: %s -> %s' % (src, dst))
|
||||||
|
|
||||||
|
|
||||||
|
def get_ssh_key_setup(ssh_key): # type: (SshKey) -> str
|
||||||
|
"""Generate and return a script to configure SSH keys on a host."""
|
||||||
|
template = ShellScriptTemplate(read_text_file(os.path.join(ANSIBLE_TEST_DATA_ROOT, 'setup', 'ssh-keys.sh')))
|
||||||
|
|
||||||
|
ssh_keys_sh = template.substitute(
|
||||||
|
ssh_public_key=ssh_key.pub_contents,
|
||||||
|
ssh_private_key=ssh_key.key_contents,
|
||||||
|
ssh_key_type=ssh_key.KEY_TYPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ssh_keys_sh
|
||||||
|
|
||||||
|
|
||||||
def get_network_settings(args, platform, version): # type: (NetworkIntegrationConfig, str, str) -> NetworkPlatformSettings
|
def get_network_settings(args, platform, version): # type: (NetworkIntegrationConfig, str, str) -> NetworkPlatformSettings
|
||||||
"""Returns settings for the given network platform and version."""
|
"""Returns settings for the given network platform and version."""
|
||||||
platform_version = '%s/%s' % (platform, version)
|
platform_version = '%s/%s' % (platform, version)
|
||||||
|
|
|
@ -5,6 +5,7 @@ __metaclass__ = type
|
||||||
import atexit
|
import atexit
|
||||||
import contextlib
|
import contextlib
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
@ -29,6 +30,7 @@ from .util import (
|
||||||
read_lines_without_comments,
|
read_lines_without_comments,
|
||||||
ANSIBLE_TEST_DATA_ROOT,
|
ANSIBLE_TEST_DATA_ROOT,
|
||||||
ApplicationError,
|
ApplicationError,
|
||||||
|
cmd_quote,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .io import (
|
from .io import (
|
||||||
|
@ -49,6 +51,19 @@ REMOTE_COMPLETION = {} # type: t.Dict[str, t.Dict[str, str]]
|
||||||
NETWORK_COMPLETION = {} # type: t.Dict[str, t.Dict[str, str]]
|
NETWORK_COMPLETION = {} # type: t.Dict[str, t.Dict[str, str]]
|
||||||
|
|
||||||
|
|
||||||
|
class ShellScriptTemplate:
|
||||||
|
"""A simple substition template for shell scripts."""
|
||||||
|
def __init__(self, template): # type: (str) -> None
|
||||||
|
self.template = template
|
||||||
|
|
||||||
|
def substitute(self, **kwargs):
|
||||||
|
"""Return a string templated with the given arguments."""
|
||||||
|
kvp = dict((k, cmd_quote(v)) for k, v in kwargs.items())
|
||||||
|
pattern = re.compile(r'#{(?P<name>[^}]+)}')
|
||||||
|
value = pattern.sub(lambda match: kvp[match.group('name')], self.template)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class ResultType:
|
class ResultType:
|
||||||
"""Test result type."""
|
"""Test result type."""
|
||||||
BOT = None # type: ResultType
|
BOT = None # type: ResultType
|
||||||
|
|
Loading…
Add table
Reference in a new issue