# This file is part of Cockpit.
#
# Copyright (C) 2013 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see .
import os
import time
import socket
import subprocess
import tempfile
import select
import errno
import sys
from . import exceptions
from . import timeout as timeoutlib
from .directories import get_temp_dir
class SSHConnection(object):
def __init__(self, user, address, ssh_port, identity_file, verbose=False):
self.verbose = verbose
# Currently all images are x86_64. When that changes we will have
# an override file for those images that are not
self.ssh_user = user
self.identity_file = identity_file
self.ssh_address = address
self.ssh_port = ssh_port
self.ssh_master = None
self.ssh_process = None
self.ssh_reachable = False
self.label = "{}@{}:{}".format(self.ssh_user, self.ssh_address, self.ssh_port)
def disconnect(self):
self.ssh_reachable = False
self._kill_ssh_master()
def message(self, *args):
"""Prints args if in verbose mode"""
if not self.verbose:
return
print(" ".join(args))
# wait until we can execute something on the machine. ie: wait for ssh
def wait_execute(self, timeout_sec=120):
"""Try to connect to self.address on ssh port"""
# If connected to machine, kill master connection
self._kill_ssh_master()
start_time = time.time()
while (time.time() - start_time) < timeout_sec:
addrinfo = socket.getaddrinfo(self.ssh_address, self.ssh_port, 0, socket.SOCK_STREAM)
(family, socktype, proto, canonname, sockaddr) = addrinfo[0]
sock = socket.socket(family, socktype, proto)
sock.settimeout(5)
try:
sock.connect(sockaddr)
data = sock.recv(10)
if len(data):
self.ssh_reachable = True
return True
except IOError:
pass
finally:
sock.close()
time.sleep(0.5)
return False
def wait_user_login(self):
"""Wait until logging in as non-root works.
Most tests run as the "admin" user, so we make sure that
user sessions are allowed (and cockit-ws will let "admin"
in) before declaring a test machine as "booted".
Returns the boot id of the system, or None if ssh timed out.
"""
tries_left = 60
while (tries_left > 0):
try:
with timeoutlib.Timeout(seconds=30):
return self.execute("! test -f /run/nologin && cat /proc/sys/kernel/random/boot_id", direct=True)
except subprocess.CalledProcessError:
pass
except RuntimeError:
# timeout; assume that ssh just went down during reboot, go back to wait_boot()
return None
tries_left = tries_left - 1
time.sleep(1)
raise exceptions.Failure("Timed out waiting for /run/nologin to disappear")
def wait_boot(self, timeout_sec=120):
"""Wait for a machine to boot"""
start_time = time.time()
boot_id = None
while (time.time() - start_time) < timeout_sec:
if self.wait_execute(timeout_sec=15):
boot_id = self.wait_user_login()
if boot_id:
break
if not boot_id:
raise exceptions.Failure("Unable to reach machine {0} via ssh: {1}:{2}".format(self.label, self.ssh_address, self.ssh_port))
self.boot_id = boot_id
def wait_reboot(self, timeout_sec=180):
self.disconnect()
assert self.boot_id, "Before using wait_reboot() use wait_boot() successfully"
boot_id = self.boot_id
start_time = time.time()
while (time.time() - start_time) < timeout_sec:
try:
self.wait_boot(timeout_sec=timeout_sec)
if self.boot_id != boot_id:
break
except exceptions.Failure:
pass
else:
raise exceptions.Failure("Timeout waiting for system to reboot properly")
def _start_ssh_master(self):
self._kill_ssh_master()
control = os.path.join(get_temp_dir(), "ssh-%h-%p-%r-" + str(os.getpid()))
cmd = [
"ssh",
"-p", str(self.ssh_port),
"-i", self.identity_file,
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "BatchMode=yes",
"-M", # ControlMaster, no stdin
"-o", "ControlPath=" + control,
"-o", "LogLevel=ERROR",
"-l", self.ssh_user,
self.ssh_address,
"/bin/bash -c 'echo READY; read a'"
]
# Connection might be refused, so try this 10 times
tries_left = 10
while tries_left > 0:
tries_left = tries_left - 1
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
stdout_fd = proc.stdout.fileno()
output = ""
while stdout_fd > -1 and "READY" not in output:
ret = select.select([stdout_fd], [], [], 10)
for fd in ret[0]:
if fd == stdout_fd:
data = os.read(fd, 1024)
if not data:
stdout_fd = -1
proc.stdout.close()
output += data.decode('utf-8', 'replace')
if stdout_fd > -1:
break
# try again if the connection was refused, unless we've used up our tries
proc.wait()
if proc.returncode == 255 and tries_left > 0:
self.message("ssh: connection refused, trying again")
time.sleep(1)
continue
else:
raise exceptions.Failure("SSH master process exited with code: {0}".format(proc.returncode))
self.ssh_master = control
self.ssh_process = proc
if not self._check_ssh_master():
raise exceptions.Failure("Couldn't launch an SSH master process")
def _kill_ssh_master(self):
if self.ssh_master:
try:
os.unlink(self.ssh_master)
except OSError as e:
if e.errno != errno.ENOENT:
raise
self.ssh_master = None
if self.ssh_process:
self.message("killing ssh master process", str(self.ssh_process.pid))
self.ssh_process.stdin.close()
self.ssh_process.terminate()
self.ssh_process.stdout.close()
with timeoutlib.Timeout(seconds=90, error_message="Timeout while waiting for ssh master to shut down"):
self.ssh_process.wait()
self.ssh_process = None
def _check_ssh_master(self):
if not self.ssh_master:
return False
cmd = [
"ssh",
"-q",
"-p", str(self.ssh_port),
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "BatchMode=yes",
"-S", self.ssh_master,
"-O", "check",
"-l", self.ssh_user,
self.ssh_address
]
with open(os.devnull, 'w') as devnull:
code = subprocess.call(cmd, stdin=devnull, stdout=devnull, stderr=devnull)
if code == 0:
self.ssh_reachable = True
return True
return False
def _ensure_ssh_master(self):
if not self._check_ssh_master():
self._start_ssh_master()
def execute(self, command=None, script=None, input=None, environment={},
stdout=None, quiet=False, direct=False, timeout=120,
ssh_env=["env", "-u", "LANGUAGE", "LC_ALL=C"]):
"""Execute a shell command in the test machine and return its output.
Either specify @command or @script
Arguments:
command: The string to execute by /bin/sh.
script: A multi-line script to execute in /bin/sh
input: Input to send to the command
environment: Additional environment variables
timeout: Applies if not already wrapped in a #Timeout context
Returns:
The command/script output as a string.
"""
assert command or script
assert self.ssh_address
if not direct:
self._ensure_ssh_master()
env_script = ""
env_command = []
if environment and isinstance(environment, dict):
for name, value in environment.items():
env_script += "%s='%s'\n" % (name, value)
env_script += "export %s\n" % name
env_command.append("{}={}".format(name, value))
elif environment == {}:
pass
else:
raise Exception("enviroment support dict or list items given: ".format(environment))
default_ssh_params = [
"ssh",
"-p", str(self.ssh_port),
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "LogLevel=ERROR",
"-o", "BatchMode=yes",
"-l", self.ssh_user,
self.ssh_address
]
additional_ssh_params = []
cmd = []
if direct:
additional_ssh_params += ["-i", self.identity_file]
else:
additional_ssh_params += ["-o", "ControlPath=" + self.ssh_master]
if command:
if getattr(command, "strip", None): # Is this a string?
cmd += [command]
if not quiet:
self.message("+", command)
else:
cmd += command
if not quiet:
self.message("+", *command)
else:
assert not input, "input not supported to script"
cmd += ["sh", "-s"]
if self.verbose:
cmd += ["-x"]
input = env_script
input += script
command = "