1import os 2import shutil 3import subprocess 4from unittest import SkipTest 5 6# subprocess does not kill the child daemon when a test case fails by raising 7# an exception. So use pexpect instead. 8import pexpect 9 10import infra 11 12 13SSHD_PORT_INITIAL = 2222 14SSHD_PORT_LAST = SSHD_PORT_INITIAL + 99 15SSHD_PATH = "/usr/sbin/sshd" 16SSHD_HOST_DIR = "host" 17 18# SSHD_KEY_DIR is where the /etc/ssh/ssh_host_*_key files go 19SSHD_KEY_DIR = os.path.join(SSHD_HOST_DIR, "etc/ssh") 20SSHD_KEY = os.path.join(SSHD_KEY_DIR, "ssh_host_ed25519_key") 21 22# SSH_CLIENT_KEY_DIR is where the client id_rsa key and authorized_keys files go 23SSH_CLIENT_KEY_DIR = os.path.join(SSHD_HOST_DIR, "home/br-user/ssh") 24SSH_CLIENT_KEY = os.path.join(SSH_CLIENT_KEY_DIR, "id_rsa") 25SSH_AUTH_KEYS_FILE = os.path.join(SSH_CLIENT_KEY_DIR, "authorized_keys") 26 27 28class OpenSSHDaemon(): 29 30 def __init__(self, builddir, logtofile): 31 """ 32 Start an OpenSSH SSH Daemon 33 34 In order to support test cases in parallel, select the port the 35 server will listen to in runtime. Since there is no reliable way 36 to allocate the port prior to starting the server (another 37 process in the host machine can use the port between it is 38 selected from a list and it is really allocated to the server) 39 try to start the server in a port and in the case it is already 40 in use, try the next one in the allowed range. 41 """ 42 self.daemon = None 43 self.port = None 44 45 self.logfile = infra.open_log_file(builddir, "sshd", logtofile) 46 47 server_keyfile = os.path.join(builddir, SSHD_KEY) 48 auth_keys_file = os.path.join(builddir, SSH_AUTH_KEYS_FILE) 49 daemon_cmd = [SSHD_PATH, 50 "-D", # or use -ddd to debug 51 "-e", 52 "-h", server_keyfile, 53 "-f", "/dev/null", 54 "-o", "ListenAddress=localhost", 55 "-o", "PidFile=none", 56 "-o", "AuthenticationMethods=publickey", 57 "-o", "StrictModes=no", 58 "-o", "Subsystem=sftp internal-sftp", 59 "-o", "AuthorizedKeysFile={}".format(auth_keys_file)] 60 for port in range(SSHD_PORT_INITIAL, SSHD_PORT_LAST + 1): 61 cmd = daemon_cmd + ["-p", "{}".format(port)] 62 self.logfile.write( 63 "> starting sshd with '{}'\n".format(" ".join(cmd))) 64 try: 65 self.daemon = pexpect.spawn(cmd[0], cmd[1:], logfile=self.logfile, 66 encoding='utf-8') 67 except pexpect.exceptions.ExceptionPexpect as e: 68 self.logfile.write("> {} - skipping\n".format(e)) 69 raise SkipTest(str(e)) 70 71 ret = self.daemon.expect([ 72 # Success 73 "Server listening on .* port {}.".format(port), 74 # Failure 75 "Cannot bind any address."]) 76 if ret == 0: 77 self.port = port 78 return 79 raise SystemError("Could not find a free port to run sshd") 80 81 def stop(self): 82 if self.daemon is None: 83 return 84 self.daemon.terminate(force=True) 85 86 87def generate_keys_server(builddir, logfile): 88 """Generate keys required to run an OpenSSH Daemon.""" 89 keyfile = os.path.join(builddir, SSHD_KEY) 90 if os.path.exists(keyfile): 91 logfile.write("> SSH server key already exists '{}'".format(keyfile)) 92 return 93 94 hostdir = os.path.join(builddir, SSHD_HOST_DIR) 95 keydir = os.path.join(builddir, SSHD_KEY_DIR) 96 os.makedirs(hostdir, exist_ok=True) 97 os.makedirs(keydir, exist_ok=True) 98 99 cmd = ["ssh-keygen", "-A", "-f", hostdir] 100 logfile.write( 101 "> generating SSH server keys with '{}'\n".format(" ".join(cmd))) 102 # When ssh-keygen fails to create an SSH server key it doesn't return a 103 # useful error code. So use check for an error message in the output 104 # instead. 105 try: 106 out = subprocess.check_output(cmd, encoding='utf-8') 107 except FileNotFoundError: 108 logfile.write("> ssh-keygen not found - skipping\n") 109 raise SkipTest("ssh-keygen not found") 110 111 logfile.write(out) 112 if "Could not save your public key" in out: 113 raise SystemError("Could not generate SSH server keys") 114 115 116def generate_keys_client(builddir, logfile): 117 """Generate keys required to log into an OpenSSH Daemon via SCP or SFTP.""" 118 keyfile = os.path.join(builddir, SSH_CLIENT_KEY) 119 if os.path.exists(keyfile): 120 logfile.write("> SSH client key already exists '{}'".format(keyfile)) 121 return 122 123 keydir = os.path.join(builddir, SSH_CLIENT_KEY_DIR) 124 os.makedirs(keydir, exist_ok=True) 125 126 cmd = ["ssh-keygen", 127 "-f", keyfile, 128 "-b", "2048", 129 "-t", "rsa", 130 "-N", "", 131 "-q"] 132 logfile.write( 133 "> generating SSH client keys with '{}'\n".format(" ".join(cmd))) 134 try: 135 subprocess.check_call(cmd, stdout=logfile, stderr=logfile) 136 except FileNotFoundError: 137 logfile.write("> ssh-keygen not found - skipping\n") 138 raise SkipTest("ssh-keygen not found") 139 140 # Allow key-based login for this user (so that we can fetch from localhost) 141 pubkeyfile = os.path.join(keydir, "{}.pub".format(keyfile)) 142 authfile = os.path.join(keydir, "authorized_keys") 143 shutil.copy(pubkeyfile, authfile) 144 145 146def generate_keys(builddir, logtofile): 147 logfile = infra.open_log_file(builddir, "ssh-keygen", logtofile) 148 generate_keys_server(builddir, logfile) 149 generate_keys_client(builddir, logfile) 150