parent
044b8da55a
commit
759617c9c0
288 changed files with 13040 additions and 1 deletions
BIN
bots/machine/cloud-init.iso
Normal file
BIN
bots/machine/cloud-init.iso
Normal file
Binary file not shown.
27
bots/machine/host_key
Normal file
27
bots/machine/host_key
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAr+bCynyw7hAG03Bwt3joCTPjrexdO+ynsA+HtZRs38N9NCaO
|
||||
MZ7j7KCRFUgkezo7GEAp7lRparZWrzAixcyATZNOokwYP55flvsWtwhTSE2wI4gY
|
||||
n+0nmNFy+l3qs29VWFzVX8CkCqXBiGw53uo8qLuMEWVdXmstNxR00pHdvlyjOhjl
|
||||
BpZBFKD8gMGDx6qClGIosgcSbNtJf6Xl1ceo7BoLNknOoJdyiT9EwdhO53A9aVhx
|
||||
kbYjbIRRVWq8P2Cq/kbPioYlUtwgAH2A4aQTVlzsEyssdnriYwIbERddG8eqZ7mn
|
||||
UhKU/FH6Of2BSFQA9Rh6bVC0s1Y1KCZupLaBwQIDAQABAoIBAQCbjHLA4NcNDjsb
|
||||
CxmCBXcbfDlged5QuYvoEzOtDN3iWlsDnPytQJbJj4v8x9kK54mOfl8WFKtL5IZv
|
||||
UR/OznK/Jv6oYqYmzAQ33T5PCRusmpaiNR2hfvQ/HSiR4i9EEbXk9+LwU8g8aivk
|
||||
WeArEfQmOgM49uxELH7FcF+GPdtbE9TsHNdkVf1CzCMcGdIeNjeCqEDQrgRSdAq8
|
||||
YpCrlvQj76Gv8g6IOUiMZYS/fqbuvMR/XryXSQkEUX/4I5QojZOD1XrzxGA94jJ9
|
||||
dOv3Yr1y2+fhPAy5dDIFoqWSuDMib2yGV47jFo+Mqu6ovLVPAt74UWucHKImXgo1
|
||||
qvl0wFkJAoGBANwmWqJZ8dxJTU5gcK2KOq3u2JUYSYek3HMkEsPjEezGtht1Okg5
|
||||
TjxFEiw+yc4yeUtj0lIOyNU976FA0+5ItiW18/Byw6zWUi2BmrLGsCM0/CL/xwKM
|
||||
hVo8DrMXcGrZY6ZSqNiLtAYLmgAUKkJEP+pw8r1Qr0pO5yfVHNeK0v/3AoGBAMyL
|
||||
xWIhETGKkmyCuqFSFPELxbmMwjqWagNrzFK23/cqgbFv0aCz6wXhcwQ5JiszFq7B
|
||||
Hvz8Wezl9Ur2FdFz3wGz46q+Cdqnw7uQTTGd5WbDWHN/tCS67bKn998BqENpPiWK
|
||||
OIgNFXnNcucFtte9o7QDmjSaDd4u0xwveRYwHg4HAoGBAJWPbOV864X3OpCzjfkn
|
||||
vmOprvPjUxjW1HlYmXMA0Y2lFdSjmFu2qsLhPc5XPaxat/KStzDOIHxWHnTTYOcx
|
||||
+KS37yh8HxlNZPjLYrhvqPvSJDT2xVGi+3lo8aeTlejRFRTKdTDgAAZXXWEOUgNA
|
||||
8Jcp8o7QwLVf00RJUNXR1zTTAoGAChy+3WMVHoXjR0oPP/p23pPeapXy5EKbax/h
|
||||
MhWobOfFEaidjHxYminTLdpFcM1NycXyaj9vkq6rudEAsyIvXD4wezh59D1nB9bS
|
||||
eil8NeBidxNRLJ+xMKvtLTE/yFVjpSd4NAGxlhv6GkHGEFRny3aCISecl+douHQA
|
||||
YIBwe/ECgYALzEEkESm8d5Zq2fuFtUhRqFGcOtr/IYR8OgtUIZe2NRImsR+r5ycN
|
||||
w4mw7RAnxKqOoXeAtWBwi5MykItiaof2MD3MIe4kxlZQt0NPpyE4dkzsUkYf89kE
|
||||
ndu5mUalV7s7KBttm9gn8e+btzERna2VPRfDQh8nHw/zLXtE7lFSUg==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
1
bots/machine/host_key.pub
Normal file
1
bots/machine/host_key.pub
Normal file
|
|
@ -0,0 +1 @@
|
|||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCv5sLKfLDuEAbTcHC3eOgJM+Ot7F077KewD4e1lGzfw300Jo4xnuPsoJEVSCR7OjsYQCnuVGlqtlavMCLFzIBNk06iTBg/nl+W+xa3CFNITbAjiBif7SeY0XL6Xeqzb1VYXNVfwKQKpcGIbDne6jyou4wRZV1eay03FHTSkd2+XKM6GOUGlkEUoPyAwYPHqoKUYiiyBxJs20l/peXVx6jsGgs2Sc6gl3KJP0TB2E7ncD1pWHGRtiNshFFVarw/YKr+Rs+KhiVS3CAAfYDhpBNWXOwTKyx2euJjAhsRF10bx6pnuadSEpT8Ufo5/YFIVAD1GHptULSzVjUoJm6ktoHB
|
||||
27
bots/machine/identity
Normal file
27
bots/machine/identity
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpQIBAAKCAQEA1DrTSXQRF8isQQfPfK3U+eFC4zBrjur+Iy15kbHUYUeSHf5S
|
||||
jXPYbHYqD1lHj4GJajC9okle9rykKFYZMmJKXLI6987wZ8vfucXo9/kwS6BDAJto
|
||||
ZpZSj5sWCQ1PI0Ce8CbkazlTp5NIkjRfhXGP8mkNKMEhdNjaYceO49ilnNCIxhpb
|
||||
eH5dH5hybmQQNmnzf+CGCCLBFmc4g3sFbWhI1ldyJzES5ZX3ahjJZYRUfnndoUM/
|
||||
TzdkHGqZhL1EeFAsv5iV65HuYbchch4vBAn8jDMmHh8G1ixUCL3uAlosfarZLLyo
|
||||
3HrZ8U/llq7rXa93PXHyI/3NL/2YP3OMxE8baQIDAQABAoIBAQCxuOUwkKqzsQ9W
|
||||
kdTWArfj3RhnKigYEX9qM+2m7TT9lbKtvUiiPc2R3k4QdmIvsXlCXLigyzJkCsqp
|
||||
IJiPEbJV98bbuAan1Rlv92TFK36fBgC15G5D4kQXD/ce828/BSFT2C3WALamEPdn
|
||||
v8Xx+Ixjokcrxrdeoy4VTcjB0q21J4C2wKP1wEPeMJnuTcySiWQBdAECCbeZ4Vsj
|
||||
cmRdcvL6z8fedRPtDW7oec+IPkYoyXPktVt8WsQPYkwEVN4hZVBneJPCcuhikYkp
|
||||
T3WGmPV0MxhUvCZ6hSG8D2mscZXRq3itXVlKJsUWfIHaAIgGomWrPuqC23rOYCdT
|
||||
5oSZmTvFAoGBAPs1FbbxDDd1fx1hisfXHFasV/sycT6ggP/eUXpBYCqVdxPQvqcA
|
||||
ktplm5j04dnaQJdHZ8TPlwtL+xlWhmhFhlCFPtVpU1HzIBkp6DkSmmu0gvA/i07Z
|
||||
pzo5Z+HRZFzruTQx6NjDtvWwiXVLwmZn2oiLeM9xSqPu55OpITifEWNjAoGBANhH
|
||||
XwV6IvnbUWojs7uiSGsXuJOdB1YCJ+UF6xu8CqdbimaVakemVO02+cgbE6jzpUpo
|
||||
krbDKOle4fIbUYHPeyB0NMidpDxTAPCGmiJz7BCS1fCxkzRgC+TICjmk5zpaD2md
|
||||
HCrtzIeHNVpTE26BAjOIbo4QqOHBXk/WPen1iC3DAoGBALsD3DSj46puCMJA2ebI
|
||||
2EoWaDGUbgZny2GxiwrvHL7XIx1XbHg7zxhUSLBorrNW7nsxJ6m3ugUo/bjxV4LN
|
||||
L59Gc27ByMvbqmvRbRcAKIJCkrB1Pirnkr2f+xx8nLEotGqNNYIawlzKnqr6SbGf
|
||||
Y2wAGWKmPyEoPLMLWLYkhfdtAoGANsFa/Tf+wuMTqZuAVXCwhOxsfnKy+MNy9jiZ
|
||||
XVwuFlDGqVIKpjkmJyhT9KVmRM/qePwgqMSgBvVOnszrxcGRmpXRBzlh6yPYiQyK
|
||||
2U4f5dJG97j9W7U1TaaXcCCfqdZDMKnmB7hMn8NLbqK5uLBQrltMIgt1tjIOfofv
|
||||
BNx0raECgYEApAvjwDJ75otKz/mvL3rUf/SNpieODBOLHFQqJmF+4hrSOniHC5jf
|
||||
f5GS5IuYtBQ1gudBYlSs9fX6T39d2avPsZjfvvSbULXi3OlzWD8sbTtvQPuCaZGI
|
||||
Df9PUWMYZ3HRwwdsYovSOkT53fG6guy+vElUEDkrpZYczROZ6GUcx70=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
1
bots/machine/identity.pub
Normal file
1
bots/machine/identity.pub
Normal file
|
|
@ -0,0 +1 @@
|
|||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUOtNJdBEXyKxBB898rdT54ULjMGuO6v4jLXmRsdRhR5Id/lKNc9hsdioPWUePgYlqML2iSV72vKQoVhkyYkpcsjr3zvBny9+5xej3+TBLoEMAm2hmllKPmxYJDU8jQJ7wJuRrOVOnk0iSNF+FcY/yaQ0owSF02Nphx47j2KWc0IjGGlt4fl0fmHJuZBA2afN/4IYIIsEWZziDewVtaEjWV3InMRLllfdqGMllhFR+ed2hQz9PN2QcapmEvUR4UCy/mJXrke5htyFyHi8ECfyMMyYeHwbWLFQIve4CWix9qtksvKjcetnxT+WWrutdr3c9cfIj/c0v/Zg/c4zETxtp cockpit-test
|
||||
1
bots/machine/machine_core/__init__.py
Normal file
1
bots/machine/machine_core/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Place holder for python module
|
||||
49
bots/machine/machine_core/cli.py
Normal file
49
bots/machine/machine_core/cli.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import signal
|
||||
import argparse
|
||||
from . import machine_virtual
|
||||
|
||||
def cmd_cli():
|
||||
parser = argparse.ArgumentParser(description="Run a VM image until SIGTERM or SIGINT")
|
||||
parser.add_argument("--memory", type=int, default=1024,
|
||||
help="Memory in MiB to allocate to the VM (default: %(default)s)")
|
||||
parser.add_argument("image", help="Image name")
|
||||
args = parser.parse_args()
|
||||
|
||||
network = machine_virtual.VirtNetwork(0, image=args.image)
|
||||
machine = machine_virtual.VirtMachine(image=args.image, networking=network.host(), memory_mb=args.memory)
|
||||
machine.start()
|
||||
machine.wait_boot()
|
||||
|
||||
# run a command to force starting the SSH master
|
||||
machine.execute('uptime')
|
||||
|
||||
# print ssh command
|
||||
print("ssh -o ControlPath=%s -p %s %s@%s" %
|
||||
(machine.ssh_master, machine.ssh_port, machine.ssh_user, machine.ssh_address))
|
||||
# print Cockpit web address
|
||||
print("http://%s:%s" % (machine.web_address, machine.web_port))
|
||||
# print marker that the VM is ready; tests can poll for this to wait for the VM
|
||||
print("RUNNING")
|
||||
|
||||
signal.signal(signal.SIGTERM, lambda sig, frame: machine.stop())
|
||||
try:
|
||||
signal.pause()
|
||||
except KeyboardInterrupt:
|
||||
machine.stop()
|
||||
35
bots/machine/machine_core/constants.py
Normal file
35
bots/machine/machine_core/constants.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
# Images which are Atomic based
|
||||
ATOMIC_IMAGES = ["rhel-atomic", "fedora-atomic", "continuous-atomic"]
|
||||
|
||||
MACHINE_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
BOTS_DIR = os.path.dirname(MACHINE_DIR)
|
||||
BASE_DIR = os.path.dirname(BOTS_DIR)
|
||||
TEST_DIR = os.path.join(BASE_DIR, "test")
|
||||
GIT_DIR = os.path.join(BASE_DIR, ".git")
|
||||
|
||||
IMAGES_DIR = os.path.join(BOTS_DIR, "images")
|
||||
SCRIPTS_DIR = os.path.join(IMAGES_DIR, "scripts")
|
||||
|
||||
DEFAULT_IDENTITY_FILE = os.path.join(MACHINE_DIR, "identity")
|
||||
|
||||
TEST_OS_DEFAULT = "fedora-30"
|
||||
DEFAULT_IMAGE = os.environ.get("TEST_OS", TEST_OS_DEFAULT)
|
||||
55
bots/machine/machine_core/directories.py
Normal file
55
bots/machine/machine_core/directories.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# This file is part of Cockpit.
|
||||
#
|
||||
# Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from .constants import BOTS_DIR, BASE_DIR, GIT_DIR
|
||||
|
||||
def get_git_config(variable):
|
||||
if not os.path.exists(GIT_DIR):
|
||||
return None
|
||||
|
||||
try:
|
||||
myenv = os.environ.copy()
|
||||
myenv["GIT_DIR"] = GIT_DIR
|
||||
return subprocess.check_output(["git", "config", variable], universal_newlines=True, env=myenv).strip()
|
||||
|
||||
except (OSError, subprocess.CalledProcessError): # 'git' not in PATH, or cmd fails
|
||||
return None
|
||||
|
||||
_images_data_dir = None
|
||||
def get_images_data_dir():
|
||||
global _images_data_dir
|
||||
|
||||
if _images_data_dir is None:
|
||||
_images_data_dir = get_git_config('cockpit.bots.images-data-dir')
|
||||
|
||||
if _images_data_dir is None:
|
||||
_images_data_dir = os.path.join(os.environ.get("TEST_DATA", BOTS_DIR), "images")
|
||||
|
||||
return _images_data_dir
|
||||
|
||||
_temp_dir = None
|
||||
def get_temp_dir():
|
||||
global _temp_dir
|
||||
|
||||
if _temp_dir is None:
|
||||
_temp_dir = os.path.join(os.environ.get("TEST_DATA", BASE_DIR), "tmp")
|
||||
os.makedirs(_temp_dir, exist_ok=True)
|
||||
|
||||
return _temp_dir
|
||||
27
bots/machine/machine_core/exceptions.py
Normal file
27
bots/machine/machine_core/exceptions.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
class Failure(Exception):
|
||||
def __init__(self, msg):
|
||||
self.msg = msg
|
||||
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
|
||||
class RepeatableFailure(Failure):
|
||||
pass
|
||||
280
bots/machine/machine_core/machine.py
Normal file
280
bots/machine/machine_core/machine.py
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from . import ssh_connection
|
||||
from . import timeout
|
||||
from .constants import DEFAULT_IDENTITY_FILE, ATOMIC_IMAGES, TEST_DIR
|
||||
|
||||
LOGIN_MESSAGE = """
|
||||
TTY LOGIN
|
||||
User: {ssh_user}/admin Password: foobar
|
||||
To quit use Ctrl+], Ctrl+5 (depending on locale)
|
||||
|
||||
SSH ACCESS
|
||||
$ ssh -p {ssh_port} {ssh_user}@{ssh_address}
|
||||
Password: foobar
|
||||
|
||||
COCKPIT
|
||||
http://{web_address}:{web_port}
|
||||
"""
|
||||
|
||||
RESOLV_SCRIPT = """
|
||||
set -e
|
||||
# HACK: Racing with operating systems reading/updating resolv.conf and
|
||||
# the fact that resolv.conf can be a symbolic link. Avoid failures like:
|
||||
# chattr: Operation not supported while reading flags on /etc/resolv.conf
|
||||
mkdir -p /etc/NetworkManager/conf.d
|
||||
printf '[main]\ndns=none\n' > /etc/NetworkManager/conf.d/dns.conf
|
||||
systemctl reload-or-restart NetworkManager
|
||||
printf 'domain {domain}\nsearch {domain}\nnameserver {nameserver}\n' >/etc/resolv2.conf
|
||||
chcon -v unconfined_u:object_r:net_conf_t:s0 /etc/resolv2.conf 2> /dev/null || true
|
||||
mv /etc/resolv2.conf /etc/resolv.conf
|
||||
"""
|
||||
|
||||
|
||||
class Machine(ssh_connection.SSHConnection):
|
||||
def __init__(self, address="127.0.0.1", image="unknown", verbose=False, label=None, browser=None,
|
||||
user="root", identity_file=None, arch="x86_64", ssh_port=22, web_port=9090):
|
||||
|
||||
identity_file_old = identity_file
|
||||
identity_file = identity_file or DEFAULT_IDENTITY_FILE
|
||||
|
||||
if identity_file_old is None:
|
||||
os.chmod(identity_file, 0o600)
|
||||
if ":" in address:
|
||||
(ssh_address, unused, ssh_port) = address.rpartition(":")
|
||||
else:
|
||||
ssh_address = address
|
||||
ssh_port = ssh_port
|
||||
if not browser:
|
||||
browser = address
|
||||
|
||||
super(Machine, self).__init__(user, ssh_address, ssh_port, identity_file, verbose=verbose)
|
||||
|
||||
self.arch = arch
|
||||
self.image = image
|
||||
self.atomic_image = self.image in ATOMIC_IMAGES
|
||||
if ":" in browser:
|
||||
(self.web_address, unused, self.web_port) = browser.rpartition(":")
|
||||
else:
|
||||
self.web_address = browser
|
||||
self.web_port = web_port
|
||||
if label:
|
||||
self.label = label
|
||||
elif self.image is not "unknown":
|
||||
self.label = "{}-{}-{}".format(self.image, self.ssh_address, self.ssh_port)
|
||||
else:
|
||||
self.label = "{}@{}:{}".format(self.ssh_user, self.ssh_address, self.ssh_port)
|
||||
|
||||
# The Linux kernel boot_id
|
||||
self.boot_id = None
|
||||
|
||||
def diagnose(self):
|
||||
keys = {
|
||||
"ssh_user": self.ssh_user,
|
||||
"ssh_address": self.ssh_address,
|
||||
"ssh_port": self.ssh_port,
|
||||
"web_address": self.web_address,
|
||||
"web_port": self.web_port,
|
||||
}
|
||||
return LOGIN_MESSAGE.format(**keys)
|
||||
|
||||
def start(self):
|
||||
"""Overridden by machine classes to start the machine"""
|
||||
self.message("Assuming machine is already running")
|
||||
|
||||
def stop(self):
|
||||
"""Overridden by machine classes to stop the machine"""
|
||||
self.message("Not shutting down already running machine")
|
||||
|
||||
def wait_poweroff(self):
|
||||
"""Overridden by machine classes to wait for a machine to stop"""
|
||||
assert False, "Cannot wait for a machine we didn't start"
|
||||
|
||||
def kill(self):
|
||||
"""Overridden by machine classes to unconditionally kill the running machine"""
|
||||
assert False, "Cannot kill a machine we didn't start"
|
||||
|
||||
def shutdown(self):
|
||||
"""Overridden by machine classes to gracefully shutdown the running machine"""
|
||||
assert False, "Cannot shutdown a machine we didn't start"
|
||||
|
||||
def upload(self, sources, dest, relative_dir=TEST_DIR):
|
||||
"""Upload a file into the test machine
|
||||
|
||||
Arguments:
|
||||
sources: the array of paths of the file to upload
|
||||
dest: the file path in the machine to upload to
|
||||
"""
|
||||
super(Machine, self).upload(sources, dest, relative_dir)
|
||||
|
||||
def download(self, source, dest, relative_dir=TEST_DIR):
|
||||
"""Download a file from the test machine.
|
||||
"""
|
||||
super(Machine, self).download(source, dest, relative_dir)
|
||||
|
||||
def download_dir(self, source, dest, relative_dir=TEST_DIR):
|
||||
"""Download a directory from the test machine, recursively.
|
||||
"""
|
||||
super(Machine, self).download_dir(source, dest, relative_dir)
|
||||
|
||||
def journal_cursor(self):
|
||||
"""Return current journal cursor
|
||||
|
||||
This can be passed to journal_messages() or audit_messages().
|
||||
"""
|
||||
return self.execute("journalctl --show-cursor -n0 -o cat | sed 's/^.*cursor: *//'")
|
||||
|
||||
def journal_messages(self, syslog_ids, log_level, cursor=None):
|
||||
"""Return interesting journal messages"""
|
||||
|
||||
# give the OS some time to write pending log messages, to make
|
||||
# unexpected message detection more reliable; RHEL/CentOS 7 does not
|
||||
# yet know about --sync, so ignore failures
|
||||
self.execute("journalctl --sync 2>/dev/null || true; sleep 3; journalctl --sync 2>/dev/null || true")
|
||||
|
||||
# Journald does not always set trusted fields like
|
||||
# _SYSTEMD_UNIT or _EXE correctly for the last few messages of
|
||||
# a dying process, so we filter by the untrusted but reliable
|
||||
# SYSLOG_IDENTIFIER instead
|
||||
|
||||
matches = " ".join(map(lambda id: "SYSLOG_IDENTIFIER=" + id, syslog_ids))
|
||||
|
||||
# Some versions of journalctl terminate unsuccessfully when
|
||||
# the output is empty. We work around this by ignoring the
|
||||
# exit status and including error messages from journalctl
|
||||
# itself in the returned messages.
|
||||
|
||||
if cursor:
|
||||
cursor_arg = "--cursor '%s'" % cursor
|
||||
else:
|
||||
cursor_arg = ""
|
||||
|
||||
cmd = "journalctl 2>&1 %s -o cat -p %d %s || true" % (cursor_arg, log_level, matches)
|
||||
messages = self.execute(cmd).splitlines()
|
||||
if len(messages) == 1 and ("Cannot assign requested address" in messages[0]
|
||||
or "-- No entries --" in messages[0]):
|
||||
# No messages
|
||||
return [ ]
|
||||
else:
|
||||
return messages
|
||||
|
||||
def audit_messages(self, type_pref, cursor=None):
|
||||
if cursor:
|
||||
cursor_arg = "--cursor '%s'" % cursor
|
||||
else:
|
||||
cursor_arg = ""
|
||||
|
||||
cmd = "journalctl %s -o cat SYSLOG_IDENTIFIER=kernel 2>&1 | grep 'type=%s.*audit' || true" % (cursor_arg, type_pref, )
|
||||
messages = self.execute(cmd).splitlines()
|
||||
if len(messages) == 1 and "Cannot assign requested address" in messages[0]:
|
||||
messages = [ ]
|
||||
return messages
|
||||
|
||||
def get_admin_group(self):
|
||||
if "debian" in self.image or "ubuntu" in self.image:
|
||||
return "sudo"
|
||||
else:
|
||||
return "wheel"
|
||||
|
||||
def start_cockpit(self, atomic_wait_for_host=None, tls=False):
|
||||
"""Start Cockpit.
|
||||
|
||||
Cockpit is not running when the test virtual machine starts up, to
|
||||
allow you to make modifications before it starts.
|
||||
"""
|
||||
|
||||
if self.atomic_image:
|
||||
# HACK: https://bugzilla.redhat.com/show_bug.cgi?id=1228776
|
||||
# we want to run:
|
||||
# self.execute("atomic run cockpit/ws --no-tls")
|
||||
# but atomic doesn't forward the parameter, so we use the resulting command
|
||||
# also we need to wait for cockpit to be up and running
|
||||
cmd = """#!/bin/sh
|
||||
systemctl start docker &&
|
||||
"""
|
||||
if tls:
|
||||
cmd += "/usr/bin/docker run -d --privileged --pid=host -v /:/host cockpit/ws /container/atomic-run --local-ssh\n"
|
||||
else:
|
||||
cmd += "/usr/bin/docker run -d --privileged --pid=host -v /:/host cockpit/ws /container/atomic-run --local-ssh --no-tls\n"
|
||||
with timeout.Timeout(seconds=90, error_message="Timeout while waiting for cockpit/ws to start"):
|
||||
self.execute(script=cmd)
|
||||
self.wait_for_cockpit_running(atomic_wait_for_host or "localhost")
|
||||
elif tls:
|
||||
self.execute(script="""#!/bin/sh
|
||||
rm -f /etc/systemd/system/cockpit.service.d/notls.conf &&
|
||||
systemctl daemon-reload &&
|
||||
systemctl stop cockpit.service &&
|
||||
systemctl start cockpit.socket
|
||||
""")
|
||||
else:
|
||||
self.execute(script="""#!/bin/sh
|
||||
mkdir -p /etc/systemd/system/cockpit.service.d/ &&
|
||||
rm -f /etc/systemd/system/cockpit.service.d/notls.conf &&
|
||||
printf \"[Service]\nExecStartPre=-/bin/sh -c 'echo 0 > /proc/sys/kernel/yama/ptrace_scope'\nExecStart=\n%s --no-tls\n\" `systemctl cat cockpit.service | grep ExecStart=` > /etc/systemd/system/cockpit.service.d/notls.conf &&
|
||||
systemctl daemon-reload &&
|
||||
systemctl stop cockpit.service &&
|
||||
systemctl start cockpit.socket
|
||||
""")
|
||||
|
||||
def restart_cockpit(self):
|
||||
"""Restart Cockpit.
|
||||
"""
|
||||
if self.atomic_image:
|
||||
with timeout.Timeout(seconds=90, error_message="timeoutlib.Timeout while waiting for cockpit/ws to restart"):
|
||||
self.execute("docker restart `docker ps | grep cockpit/ws | awk '{print $1;}'`")
|
||||
self.wait_for_cockpit_running()
|
||||
else:
|
||||
self.execute("systemctl restart cockpit")
|
||||
|
||||
def stop_cockpit(self):
|
||||
"""Stop Cockpit.
|
||||
"""
|
||||
if self.atomic_image:
|
||||
with timeout.Timeout(seconds=60, error_message="Timeout while waiting for cockpit/ws to stop"):
|
||||
self.execute("docker kill `docker ps | grep cockpit/ws | awk '{print $1;}'`")
|
||||
else:
|
||||
self.execute("systemctl stop cockpit.socket cockpit.service")
|
||||
|
||||
def set_address(self, address, mac='52:54:01'):
|
||||
"""Set IP address for the network interface with given mac prefix"""
|
||||
cmd = "nmcli con add type ethernet autoconnect yes con-name static-{mac} ifname \"$(grep -l '{mac}' /sys/class/net/*/address | cut -d / -f 5)\" ip4 {address} && ( nmcli conn up static-{mac} || true )"
|
||||
self.execute(cmd.format(mac=mac, address=address))
|
||||
|
||||
def set_dns(self, nameserver=None, domain=None):
|
||||
self.execute(RESOLV_SCRIPT.format(nameserver=nameserver or "127.0.0.1", domain=domain or "cockpit.lan"))
|
||||
|
||||
def dhcp_server(self, mac='52:54:01', range=['10.111.112.2', '10.111.127.254']):
|
||||
"""Sets up a DHCP server on the interface"""
|
||||
cmd = "dnsmasq --domain=cockpit.lan --interface=\"$(grep -l '{mac}' /sys/class/net/*/address | cut -d / -f 5)\" --bind-dynamic --dhcp-range=" + ','.join(range) + " && firewall-cmd --add-service=dhcp"
|
||||
self.execute(cmd.format(mac=mac))
|
||||
|
||||
def dns_server(self, mac='52:54:01'):
|
||||
"""Sets up a DNS server on the interface"""
|
||||
cmd = "dnsmasq --domain=cockpit.lan --interface=\"$(grep -l '{mac}' /sys/class/net/*/address | cut -d / -f 5)\" --bind-dynamic"
|
||||
self.execute(cmd.format(mac=mac))
|
||||
|
||||
def wait_for_cockpit_running(self, address="localhost", port=9090, seconds=30, tls=False):
|
||||
WAIT_COCKPIT_RUNNING = """#!/bin/sh
|
||||
until curl --insecure --silent --connect-timeout 2 --max-time 3 %s://%s:%s >/dev/null; do
|
||||
sleep 0.5;
|
||||
done;
|
||||
""" % (tls and "https" or "http", address, port)
|
||||
with timeout.Timeout(seconds=seconds, error_message="Timeout while waiting for cockpit to start"):
|
||||
self.execute(script=WAIT_COCKPIT_RUNNING)
|
||||
686
bots/machine/machine_core/machine_virtual.py
Normal file
686
bots/machine/machine_core/machine_virtual.py
Normal file
|
|
@ -0,0 +1,686 @@
|
|||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import contextlib
|
||||
import errno
|
||||
import fcntl
|
||||
import libvirt
|
||||
import libvirt_qemu
|
||||
import os
|
||||
import string
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import sys
|
||||
import time
|
||||
|
||||
from .exceptions import Failure, RepeatableFailure
|
||||
from .machine import Machine
|
||||
from .constants import TEST_DIR, BOTS_DIR
|
||||
from .directories import get_temp_dir
|
||||
|
||||
MEMORY_MB = 1024
|
||||
|
||||
|
||||
# The Atomic variants can't build their own packages, so we build in
|
||||
# their non-Atomic siblings. For example, fedora-atomic is built
|
||||
# in fedora-29
|
||||
def get_build_image(image):
|
||||
(test_os, unused) = os.path.splitext(os.path.basename(image))
|
||||
if test_os == "fedora-atomic":
|
||||
image = "fedora-29"
|
||||
elif test_os == "rhel-atomic":
|
||||
image = "rhel-7-7"
|
||||
elif test_os == "continuous-atomic":
|
||||
image = "centos-7"
|
||||
return image
|
||||
|
||||
|
||||
# some tests have suffixes that run the same image in different modes; map a
|
||||
# test context image to an actual physical image name
|
||||
def get_test_image(image):
|
||||
return image.replace("-distropkg", "")
|
||||
|
||||
|
||||
# based on http://stackoverflow.com/a/17753573
|
||||
# we use this to quieten down calls
|
||||
@contextlib.contextmanager
|
||||
def stdchannel_redirected(stdchannel, dest_filename):
|
||||
"""
|
||||
A context manager to temporarily redirect stdout or stderr
|
||||
e.g.:
|
||||
with stdchannel_redirected(sys.stderr, os.devnull):
|
||||
noisy_function()
|
||||
"""
|
||||
try:
|
||||
stdchannel.flush()
|
||||
oldstdchannel = os.dup(stdchannel.fileno())
|
||||
dest_file = open(dest_filename, 'w')
|
||||
os.dup2(dest_file.fileno(), stdchannel.fileno())
|
||||
yield
|
||||
finally:
|
||||
if oldstdchannel is not None:
|
||||
os.dup2(oldstdchannel, stdchannel.fileno())
|
||||
if dest_file is not None:
|
||||
dest_file.close()
|
||||
|
||||
|
||||
TEST_CONSOLE_XML="""
|
||||
<console type='pty'>
|
||||
<target type='serial' port='0'/>
|
||||
</console>
|
||||
"""
|
||||
|
||||
TEST_GRAPHICS_XML="""
|
||||
<video>
|
||||
<model type='qxl' ram='65536' vram='65536' vgamem='16384' heads='1' primary='yes'/>
|
||||
<alias name='video0'/>
|
||||
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>
|
||||
</video>
|
||||
<graphics type='vnc' autoport='yes' listen='127.0.0.1'>
|
||||
<listen type='address' address='127.0.0.1'/>
|
||||
</graphics>
|
||||
"""
|
||||
|
||||
TEST_DOMAIN_XML="""
|
||||
<domain type='{type}' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
|
||||
<name>{label}</name>
|
||||
{cpu}
|
||||
<os>
|
||||
<type arch='{arch}'>hvm</type>
|
||||
<boot dev='hd'/>
|
||||
{loader}
|
||||
</os>
|
||||
<memory unit='MiB'>{memory_in_mib}</memory>
|
||||
<currentMemory unit='MiB'>{memory_in_mib}</currentMemory>
|
||||
<features>
|
||||
<acpi/>
|
||||
</features>
|
||||
<devices>
|
||||
<disk type='file' snapshot='external'>
|
||||
<driver name='qemu' type='qcow2'/>
|
||||
<source file='{drive}'/>
|
||||
<target dev='vda' bus='{disk}'/>
|
||||
<serial>ROOT</serial>
|
||||
</disk>
|
||||
<controller type='scsi' model='virtio-scsi' index='0' id='hot'/>
|
||||
{console}
|
||||
<disk type='file' device='cdrom'>
|
||||
<source file='{iso}'/>
|
||||
<target dev='hdb' bus='ide'/>
|
||||
<readonly/>
|
||||
</disk>
|
||||
<rng model='virtio'>
|
||||
<backend model='random'>/dev/urandom</backend>
|
||||
</rng>
|
||||
{bridgedev}
|
||||
</devices>
|
||||
<qemu:commandline>
|
||||
{ethernet}
|
||||
{redir}
|
||||
</qemu:commandline>
|
||||
</domain>
|
||||
"""
|
||||
|
||||
TEST_DISK_XML="""
|
||||
<disk type='file'>
|
||||
<driver name='qemu' type='%(type)s'/>
|
||||
<source file='%(file)s'/>
|
||||
<serial>%(serial)s</serial>
|
||||
<address type='drive' controller='0' bus='0' target='2' unit='%(unit)d'/>
|
||||
<target dev='%(dev)s' bus='scsi'/>
|
||||
</disk>
|
||||
"""
|
||||
|
||||
TEST_KVM_XML="""
|
||||
<cpu mode='host-passthrough'/>
|
||||
<vcpu>{cpus}</vcpu>
|
||||
"""
|
||||
|
||||
# The main network interface which we use to communicate between VMs
|
||||
TEST_MCAST_XML="""
|
||||
<qemu:arg value='-netdev'/>
|
||||
<qemu:arg value='socket,mcast=230.0.0.1:{mcast},id=mcast0'/>
|
||||
<qemu:arg value='-device'/>
|
||||
<qemu:arg value='{netdriver},netdev=mcast0,mac={mac},bus=pci.0,addr=0x0f'/>
|
||||
"""
|
||||
|
||||
TEST_BRIDGE_XML="""
|
||||
<interface type="bridge">
|
||||
<source bridge="{bridge}"/>
|
||||
<mac address="{mac}"/>
|
||||
<model type="{netdriver}"/>
|
||||
</interface>
|
||||
"""
|
||||
|
||||
# Used to access SSH from the main host into the virtual machines
|
||||
TEST_REDIR_XML="""
|
||||
<qemu:arg value='-netdev'/>
|
||||
<qemu:arg value='user,id=base0,restrict={restrict},net=172.27.0.0/24,hostname={name},{forwards}'/>
|
||||
<qemu:arg value='-device'/>
|
||||
<qemu:arg value='{netdriver},netdev=base0,bus=pci.0,addr=0x0e'/>
|
||||
"""
|
||||
|
||||
class VirtNetwork:
|
||||
def __init__(self, network=None, bridge=None, image="generic"):
|
||||
self.locked = [ ]
|
||||
self.bridge = bridge
|
||||
self.image = image
|
||||
|
||||
if network is None:
|
||||
offset = 0
|
||||
force = False
|
||||
else:
|
||||
offset = network * 100
|
||||
force = True
|
||||
|
||||
# This is a shared port used as the identifier for the socket mcast network
|
||||
self.network = self._lock(5500 + offset, step=100, force=force)
|
||||
|
||||
# An offset for other ports allocated later
|
||||
self.offset = (self.network - 5500)
|
||||
|
||||
# The last machine we allocated
|
||||
self.last = 0
|
||||
|
||||
# Unique hostnet identifiers
|
||||
self.hostnet = 8
|
||||
|
||||
def _lock(self, start, step=1, force=False):
|
||||
resources = os.path.join(tempfile.gettempdir(), ".cockpit-test-resources")
|
||||
try:
|
||||
os.mkdir(resources, 0o755)
|
||||
except FileExistsError:
|
||||
pass
|
||||
for port in range(start, start + (100 * step), step):
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
lockpath = os.path.join(resources, "network-{0}".format(port))
|
||||
try:
|
||||
lockf = os.open(lockpath, os.O_WRONLY | os.O_CREAT)
|
||||
fcntl.flock(lockf, fcntl.LOCK_NB | fcntl.LOCK_EX)
|
||||
sock.bind(("127.0.0.1", port))
|
||||
self.locked.append(lockf)
|
||||
except IOError:
|
||||
if force:
|
||||
return port
|
||||
os.close(lockf)
|
||||
continue
|
||||
else:
|
||||
return port
|
||||
finally:
|
||||
sock.close()
|
||||
raise Failure("Couldn't find unique network port number")
|
||||
|
||||
# Create resources for an interface, returns address and XML
|
||||
def interface(self, number=None):
|
||||
if number is None:
|
||||
number = self.last + 1
|
||||
if number > self.last:
|
||||
self.last = number
|
||||
mac = self._lock(10000 + self.offset + number) - (10000 + self.offset)
|
||||
hostnet = self.hostnet
|
||||
self.hostnet += 1
|
||||
result = {
|
||||
"number": self.offset + number,
|
||||
"mac": '52:54:01:{:02x}:{:02x}:{:02x}'.format((mac >> 16) & 0xff, (mac >> 8) & 0xff, mac & 0xff),
|
||||
"name": "m{0}.cockpit.lan".format(mac),
|
||||
"mcast": self.network,
|
||||
"hostnet": "hostnet{0}".format(hostnet)
|
||||
}
|
||||
return result
|
||||
|
||||
# Create resources for a host, returns address and XML
|
||||
def host(self, number=None, restrict=False, isolate=False, forward={ }):
|
||||
result = self.interface(number)
|
||||
result["mcast"] = self.network
|
||||
result["restrict"] = restrict and "on" or "off"
|
||||
result["forward"] = { "22": 2200, "9090": 9090 }
|
||||
result["forward"].update(forward)
|
||||
result["netdriver"] = ("windows" in self.image) and "rtl8139" or "virtio-net-pci"
|
||||
forwards = []
|
||||
for remote, local in result["forward"].items():
|
||||
local = self._lock(int(local) + result["number"])
|
||||
result["forward"][remote] = "127.0.0.2:{}".format(local)
|
||||
forwards.append("hostfwd=tcp:{}-:{}".format(result["forward"][remote], remote))
|
||||
if remote == "22":
|
||||
result["control"] = result["forward"][remote]
|
||||
elif remote == "9090":
|
||||
result["browser"] = result["forward"][remote]
|
||||
|
||||
if isolate:
|
||||
result["bridge"] = ""
|
||||
result["bridgedev"] = ""
|
||||
result["ethernet"] = ""
|
||||
elif self.bridge:
|
||||
result["bridge"] = self.bridge
|
||||
result["bridgedev"] = TEST_BRIDGE_XML.format(**result)
|
||||
result["ethernet"] = ""
|
||||
else:
|
||||
result["bridge"] = ""
|
||||
result["bridgedev"] = ""
|
||||
result["ethernet"] = TEST_MCAST_XML.format(**result)
|
||||
result["forwards"] = ",".join(forwards)
|
||||
result["redir"] = TEST_REDIR_XML.format(**result)
|
||||
return result
|
||||
|
||||
def kill(self):
|
||||
locked = self.locked
|
||||
self.locked = [ ]
|
||||
for x in locked:
|
||||
os.close(x)
|
||||
|
||||
class VirtMachine(Machine):
|
||||
network = None
|
||||
memory_mb = None
|
||||
cpus = None
|
||||
|
||||
def __init__(self, image, networking=None, maintain=False, memory_mb=None, cpus=None, graphics=False, **args):
|
||||
self.maintain = maintain
|
||||
|
||||
# Currently all images are run on x86_64. When that changes we will have
|
||||
# an override file for those images that are not
|
||||
self.arch = "x86_64"
|
||||
|
||||
self.memory_mb = memory_mb or VirtMachine.memory_mb or MEMORY_MB
|
||||
self.cpus = cpus or VirtMachine.cpus or 1
|
||||
self.graphics = graphics or "windows" in image
|
||||
|
||||
# Set up some temporary networking info if necessary
|
||||
if networking is None:
|
||||
networking = VirtNetwork(image=image).host()
|
||||
|
||||
# Allocate network information about this machine
|
||||
self.networking = networking
|
||||
args["address"] = networking["control"]
|
||||
args["browser"] = networking["browser"]
|
||||
self.forward = networking["forward"]
|
||||
|
||||
# The path to the image file to load, and parse an image name
|
||||
if "/" in image:
|
||||
self.image_file = image = os.path.abspath(image)
|
||||
else:
|
||||
self.image_file = os.path.join(TEST_DIR, "images", image)
|
||||
if not os.path.lexists(self.image_file):
|
||||
self.image_file = os.path.join(BOTS_DIR, "images", image)
|
||||
(image, extension) = os.path.splitext(os.path.basename(image))
|
||||
|
||||
Machine.__init__(self, image=image, **args)
|
||||
|
||||
base_dir = os.path.dirname(BOTS_DIR)
|
||||
self.run_dir = os.path.join(get_temp_dir(), "run")
|
||||
|
||||
self.virt_connection = self._libvirt_connection(hypervisor = "qemu:///session")
|
||||
|
||||
self._disks = [ ]
|
||||
self._domain = None
|
||||
|
||||
# init variables needed for running a vm
|
||||
self._cleanup()
|
||||
|
||||
def _libvirt_connection(self, hypervisor, read_only = False):
|
||||
tries_left = 5
|
||||
connection = None
|
||||
if read_only:
|
||||
open_function = libvirt.openReadOnly
|
||||
else:
|
||||
open_function = libvirt.open
|
||||
while not connection and (tries_left > 0):
|
||||
try:
|
||||
connection = open_function(hypervisor)
|
||||
except:
|
||||
# wait a bit
|
||||
time.sleep(1)
|
||||
pass
|
||||
tries_left -= 1
|
||||
if not connection:
|
||||
# try again, but if an error occurs, don't catch it
|
||||
connection = open_function(hypervisor)
|
||||
return connection
|
||||
|
||||
def _start_qemu(self):
|
||||
self._cleanup()
|
||||
|
||||
os.makedirs(self.run_dir, 0o750, exist_ok=True)
|
||||
|
||||
def execute(*args):
|
||||
self.message(*args)
|
||||
return subprocess.check_call(args)
|
||||
|
||||
image_to_use = self.image_file
|
||||
if not self.maintain:
|
||||
(unused, self._transient_image) = tempfile.mkstemp(suffix='.qcow2', prefix="", dir=self.run_dir)
|
||||
execute("qemu-img", "create", "-q", "-f", "qcow2",
|
||||
"-o", "backing_file=%s" % self.image_file, self._transient_image)
|
||||
image_to_use = self._transient_image
|
||||
|
||||
keys = {
|
||||
"label": self.label,
|
||||
"image": self.image,
|
||||
"type": "qemu",
|
||||
"arch": self.arch,
|
||||
"cpu": "",
|
||||
"cpus": self.cpus,
|
||||
"memory_in_mib": self.memory_mb,
|
||||
"drive": image_to_use,
|
||||
"iso": os.path.join(BOTS_DIR, "machine", "cloud-init.iso"),
|
||||
}
|
||||
|
||||
if os.path.exists("/dev/kvm"):
|
||||
keys["type"] = "kvm"
|
||||
keys["cpu"] = TEST_KVM_XML.format(**keys)
|
||||
else:
|
||||
sys.stderr.write("WARNING: Starting virtual machine with emulation due to missing KVM\n")
|
||||
sys.stderr.write("WARNING: Machine will run about 10-20 times slower\n")
|
||||
|
||||
keys.update(self.networking)
|
||||
keys["name"] = "{image}-{control}".format(**keys)
|
||||
|
||||
# No need or use for redir network on windows
|
||||
if "windows" in self.image:
|
||||
keys["disk"] = "ide"
|
||||
keys["redir"] = ""
|
||||
else:
|
||||
keys["disk"] = "virtio"
|
||||
if self.graphics:
|
||||
keys["console"] = TEST_GRAPHICS_XML.format(**keys)
|
||||
else:
|
||||
keys["console"] = TEST_CONSOLE_XML.format(**keys)
|
||||
if "windows-10" in self.image:
|
||||
keys["loader"] = "<loader readonly='yes' type='pflash'>/usr/share/edk2/ovmf/OVMF_CODE.fd</loader>"
|
||||
else:
|
||||
keys["loader"] = ""
|
||||
test_domain_desc = TEST_DOMAIN_XML.format(**keys)
|
||||
|
||||
# add the virtual machine
|
||||
try:
|
||||
# print >> sys.stderr, test_domain_desc
|
||||
self._domain = self.virt_connection.createXML(test_domain_desc, libvirt.VIR_DOMAIN_START_AUTODESTROY)
|
||||
except libvirt.libvirtError as le:
|
||||
if 'already exists with uuid' in str(le):
|
||||
raise RepeatableFailure("libvirt domain already exists: " + str(le))
|
||||
else:
|
||||
raise
|
||||
|
||||
# start virsh console
|
||||
def qemu_console(self, extra_message=""):
|
||||
self.message("Started machine {0}".format(self.label))
|
||||
if self.maintain:
|
||||
message = "\nWARNING: Uncontrolled shutdown can lead to a corrupted image\n"
|
||||
else:
|
||||
message = "\nWARNING: All changes are discarded, the image file won't be changed\n"
|
||||
message += self.diagnose() + extra_message + "\nlogin: "
|
||||
message = message.replace("\n", "\r\n")
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen("virsh -c qemu:///session console %s" % str(self._domain.ID()), shell=True)
|
||||
|
||||
# Fill in information into /etc/issue about login access
|
||||
pid = 0
|
||||
while pid == 0:
|
||||
if message:
|
||||
try:
|
||||
with stdchannel_redirected(sys.stderr, os.devnull):
|
||||
Machine.wait_boot(self)
|
||||
sys.stderr.write(message)
|
||||
except (Failure, subprocess.CalledProcessError):
|
||||
pass
|
||||
message = None
|
||||
(pid, ret) = os.waitpid(proc.pid, message and os.WNOHANG or 0)
|
||||
|
||||
try:
|
||||
if self.maintain:
|
||||
self.shutdown()
|
||||
else:
|
||||
self.kill()
|
||||
except libvirt.libvirtError as le:
|
||||
# the domain may have already been freed (shutdown) while the console was running
|
||||
self.message("libvirt error during shutdown: %s" % (le.get_error_message()))
|
||||
|
||||
except OSError as ex:
|
||||
raise Failure("Failed to launch virsh command: {0}".format(ex.strerror))
|
||||
finally:
|
||||
self._cleanup()
|
||||
|
||||
def graphics_console(self):
|
||||
self.message("Started machine {0}".format(self.label))
|
||||
if self.maintain:
|
||||
message = "\nWARNING: Uncontrolled shutdown can lead to a corrupted image\n"
|
||||
else:
|
||||
message = "\nWARNING: All changes are discarded, the image file won't be changed\n"
|
||||
if "bridge" in self.networking:
|
||||
message += "\nIn the machine a web browser can access Cockpit on parent host:\n\n"
|
||||
message += " https://10.111.112.1:9090\n"
|
||||
message = message.replace("\n", "\r\n")
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(["virt-viewer", str(self._domain.ID())])
|
||||
sys.stderr.write(message)
|
||||
proc.wait()
|
||||
except OSError as ex:
|
||||
raise Failure("Failed to launch virt-viewer command: {0}".format(ex.strerror))
|
||||
finally:
|
||||
self._cleanup()
|
||||
|
||||
def pull(self, image):
|
||||
if "/" in image:
|
||||
image_file = os.path.abspath(image)
|
||||
else:
|
||||
image_file = os.path.join(BOTS_DIR, "images", image)
|
||||
if not os.path.exists(image_file):
|
||||
try:
|
||||
subprocess.check_call([ os.path.join(BOTS_DIR, "image-download"), image_file ])
|
||||
except OSError as ex:
|
||||
if ex.errno != errno.ENOENT:
|
||||
raise
|
||||
return image_file
|
||||
|
||||
def start(self):
|
||||
tries = 0
|
||||
while True:
|
||||
try:
|
||||
self._start_qemu()
|
||||
if not self._domain.isActive():
|
||||
self._domain.start()
|
||||
except RepeatableFailure:
|
||||
self.kill()
|
||||
if tries < 10:
|
||||
tries += 1
|
||||
time.sleep(tries)
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
except:
|
||||
self.kill()
|
||||
raise
|
||||
|
||||
# Normally only one pass
|
||||
break
|
||||
|
||||
def _diagnose_no_address(self):
|
||||
SCRIPT = """
|
||||
spawn virsh -c qemu:///session console $argv
|
||||
set timeout 300
|
||||
expect "Escape character"
|
||||
send "\r"
|
||||
expect " login: "
|
||||
send "root\r"
|
||||
expect "Password: "
|
||||
send "foobar\r"
|
||||
expect " ~]# "
|
||||
send "ip addr\r\n"
|
||||
expect " ~]# "
|
||||
exit 0
|
||||
"""
|
||||
expect = subprocess.Popen(["expect", "--", "-", str(self._domain.ID())], stdin=subprocess.PIPE,
|
||||
universal_newlines=True)
|
||||
expect.communicate(SCRIPT)
|
||||
|
||||
def wait_boot(self, timeout_sec=120):
|
||||
"""Wait for a machine to boot"""
|
||||
try:
|
||||
Machine.wait_boot(self, timeout_sec)
|
||||
except Failure:
|
||||
self._diagnose_no_address()
|
||||
raise
|
||||
|
||||
def stop(self, timeout_sec=120):
|
||||
if self.maintain:
|
||||
self.shutdown(timeout_sec=timeout_sec)
|
||||
else:
|
||||
self.kill()
|
||||
|
||||
def _cleanup(self, quick=False):
|
||||
self.disconnect()
|
||||
try:
|
||||
for disk in self._disks:
|
||||
self.rem_disk(disk, quick)
|
||||
|
||||
self._domain = None
|
||||
if hasattr(self, '_transient_image') and self._transient_image and os.path.exists(self._transient_image):
|
||||
os.unlink(self._transient_image)
|
||||
except:
|
||||
(type, value, traceback) = sys.exc_info()
|
||||
sys.stderr.write("WARNING: Cleanup failed:%s\n" % value)
|
||||
|
||||
def kill(self):
|
||||
# stop system immediately, with potential data loss
|
||||
# to shutdown gracefully, use shutdown()
|
||||
try:
|
||||
self.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
if self._domain:
|
||||
try:
|
||||
# not graceful
|
||||
with stdchannel_redirected(sys.stderr, os.devnull):
|
||||
self._domain.destroyFlags(libvirt.VIR_DOMAIN_DESTROY_DEFAULT)
|
||||
except:
|
||||
pass
|
||||
self._cleanup(quick=True)
|
||||
|
||||
def wait_poweroff(self, timeout_sec=120):
|
||||
# shutdown must have already been triggered
|
||||
if self._domain:
|
||||
start_time = time.time()
|
||||
while (time.time() - start_time) < timeout_sec:
|
||||
try:
|
||||
with stdchannel_redirected(sys.stderr, os.devnull):
|
||||
if not self._domain.isActive():
|
||||
break
|
||||
except libvirt.libvirtError as le:
|
||||
if 'no domain' in str(le) or 'not found' in str(le):
|
||||
break
|
||||
raise
|
||||
time.sleep(1)
|
||||
else:
|
||||
raise Failure("Waiting for machine poweroff timed out")
|
||||
try:
|
||||
with stdchannel_redirected(sys.stderr, os.devnull):
|
||||
self._domain.destroyFlags(libvirt.VIR_DOMAIN_DESTROY_DEFAULT)
|
||||
except libvirt.libvirtError as le:
|
||||
if 'not found' not in str(le):
|
||||
raise
|
||||
self._cleanup(quick=True)
|
||||
|
||||
def shutdown(self, timeout_sec=120):
|
||||
# shutdown the system gracefully
|
||||
# to stop it immediately, use kill()
|
||||
self.disconnect()
|
||||
try:
|
||||
if self._domain:
|
||||
self._domain.shutdown()
|
||||
self.wait_poweroff(timeout_sec=timeout_sec)
|
||||
finally:
|
||||
self._cleanup()
|
||||
|
||||
def add_disk(self, size=None, serial=None, path=None, type='raw'):
|
||||
index = len(self._disks)
|
||||
|
||||
os.makedirs(self.run_dir, 0o750, exist_ok=True)
|
||||
|
||||
if path:
|
||||
(unused, image) = tempfile.mkstemp(suffix='.qcow2', prefix=os.path.basename(path), dir=self.run_dir)
|
||||
subprocess.check_call([ "qemu-img", "create", "-q", "-f", "qcow2",
|
||||
"-o", "backing_file=" + os.path.realpath(path), image ])
|
||||
|
||||
else:
|
||||
assert size is not None
|
||||
name = "disk-{0}".format(self._domain.name())
|
||||
(unused, image) = tempfile.mkstemp(suffix='qcow2', prefix=name, dir=self.run_dir)
|
||||
subprocess.check_call(["qemu-img", "create", "-q", "-f", "raw", image, str(size)])
|
||||
|
||||
if not serial:
|
||||
serial = "DISK{0}".format(index)
|
||||
dev = 'sd' + string.ascii_lowercase[index]
|
||||
disk_desc = TEST_DISK_XML % {
|
||||
'file': image,
|
||||
'serial': serial,
|
||||
'unit': index,
|
||||
'dev': dev,
|
||||
'type': type,
|
||||
}
|
||||
|
||||
if self._domain.attachDeviceFlags(disk_desc, libvirt.VIR_DOMAIN_AFFECT_LIVE) != 0:
|
||||
raise Failure("Unable to add disk to vm")
|
||||
|
||||
disk = {
|
||||
"path": image,
|
||||
"serial": serial,
|
||||
"filename": image,
|
||||
"dev": dev,
|
||||
"index": index,
|
||||
"type": type,
|
||||
}
|
||||
|
||||
self._disks.append(disk)
|
||||
return disk
|
||||
|
||||
def rem_disk(self, disk, quick=False):
|
||||
if not quick:
|
||||
disk_desc = TEST_DISK_XML % {
|
||||
'file': disk["filename"],
|
||||
'serial': disk["serial"],
|
||||
'unit': disk["index"],
|
||||
'dev': disk["dev"],
|
||||
'type': disk["type"]
|
||||
}
|
||||
|
||||
if self._domain:
|
||||
if self._domain.detachDeviceFlags(disk_desc, libvirt.VIR_DOMAIN_AFFECT_LIVE ) != 0:
|
||||
raise Failure("Unable to remove disk from vm")
|
||||
|
||||
def _qemu_monitor(self, command):
|
||||
self.message("& " + command)
|
||||
# you can run commands manually using virsh:
|
||||
# virsh -c qemu:///session qemu-monitor-command [domain name/id] --hmp [command]
|
||||
output = libvirt_qemu.qemuMonitorCommand(self._domain, command, libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_HMP)
|
||||
self.message(output.strip())
|
||||
return output
|
||||
|
||||
def add_netiface(self, networking=None):
|
||||
if not networking:
|
||||
networking = VirtNetwork(image=self.image).interface()
|
||||
self._qemu_monitor("netdev_add socket,mcast=230.0.0.1:{mcast},id={id}".format(mcast=networking["mcast"], id=networking["hostnet"]))
|
||||
cmd = "device_add virtio-net-pci,mac={0},netdev={1}".format(networking["mac"], networking["hostnet"])
|
||||
self._qemu_monitor("device_add virtio-net-pci,mac={0},netdev={1}".format(networking["mac"], networking["hostnet"]))
|
||||
return networking["mac"]
|
||||
|
||||
def needs_writable_usr(self):
|
||||
# On atomic systems, we need a hack to change files in /usr/lib/systemd
|
||||
if self.atomic_image:
|
||||
self.execute(command="mount -o remount,rw /usr")
|
||||
462
bots/machine/machine_core/ssh_connection.py
Normal file
462
bots/machine/machine_core/ssh_connection.py
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
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 = "<script>"
|
||||
command_line = ssh_env + default_ssh_params + additional_ssh_params + env_command + cmd
|
||||
if stdout:
|
||||
subprocess.call(command_line, stdout=stdout)
|
||||
return
|
||||
|
||||
with timeoutlib.Timeout(seconds=timeout, error_message="Timed out on '%s'" % command, machine=self):
|
||||
output = ""
|
||||
proc = subprocess.Popen(command_line, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdin_fd = proc.stdin.fileno()
|
||||
stdout_fd = proc.stdout.fileno()
|
||||
stderr_fd = proc.stderr.fileno()
|
||||
rset = [stdout_fd, stderr_fd]
|
||||
wset = [stdin_fd]
|
||||
while len(rset) > 0 or len(wset) > 0:
|
||||
ret = select.select(rset, wset, [], 10)
|
||||
for fd in ret[0]:
|
||||
if fd == stdout_fd:
|
||||
data = os.read(fd, 1024)
|
||||
if not data:
|
||||
rset.remove(stdout_fd)
|
||||
proc.stdout.close()
|
||||
else:
|
||||
if self.verbose:
|
||||
os.write(sys.__stdout__.fileno(), data)
|
||||
output += data.decode('utf-8', 'replace')
|
||||
elif fd == stderr_fd:
|
||||
data = os.read(fd, 1024)
|
||||
if not data:
|
||||
rset.remove(stderr_fd)
|
||||
proc.stderr.close()
|
||||
elif not quiet or self.verbose:
|
||||
os.write(sys.__stderr__.fileno(), data)
|
||||
for fd in ret[1]:
|
||||
if fd == stdin_fd:
|
||||
if input:
|
||||
num = os.write(fd, input.encode('utf-8'))
|
||||
input = input[num:]
|
||||
if not input:
|
||||
wset.remove(stdin_fd)
|
||||
proc.stdin.close()
|
||||
proc.wait()
|
||||
|
||||
if proc.returncode != 0:
|
||||
raise subprocess.CalledProcessError(proc.returncode, command, output=output)
|
||||
return output
|
||||
|
||||
def upload(self, sources, dest, relative_dir="."):
|
||||
"""Upload a file into the test machine
|
||||
|
||||
Arguments:
|
||||
sources: the array of paths of the file to upload
|
||||
dest: the file path in the machine to upload to
|
||||
"""
|
||||
assert sources and dest
|
||||
assert self.ssh_address
|
||||
|
||||
self._ensure_ssh_master()
|
||||
|
||||
cmd = [
|
||||
"scp", "-B",
|
||||
"-r", "-p",
|
||||
"-P", str(self.ssh_port),
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "ControlPath=" + self.ssh_master,
|
||||
"-o", "BatchMode=yes",
|
||||
]
|
||||
if not self.verbose:
|
||||
cmd += [ "-q" ]
|
||||
|
||||
def relative_to_test_dir(path):
|
||||
return os.path.join(relative_dir, path)
|
||||
cmd += map(relative_to_test_dir, sources)
|
||||
|
||||
cmd += [ "%s@[%s]:%s" % (self.ssh_user, self.ssh_address, dest) ]
|
||||
|
||||
self.message("Uploading", ", ".join(sources))
|
||||
self.message(" ".join(cmd))
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
def download(self, source, dest, relative_dir="."):
|
||||
"""Download a file from the test machine.
|
||||
"""
|
||||
assert source and dest
|
||||
assert self.ssh_address
|
||||
|
||||
self._ensure_ssh_master()
|
||||
dest = os.path.join(relative_dir, dest)
|
||||
|
||||
cmd = [
|
||||
"scp", "-B",
|
||||
"-P", str(self.ssh_port),
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "ControlPath=" + self.ssh_master,
|
||||
"-o", "BatchMode=yes",
|
||||
]
|
||||
if not self.verbose:
|
||||
cmd += ["-q"]
|
||||
cmd += [ "%s@[%s]:%s" % (self.ssh_user, self.ssh_address, source), dest ]
|
||||
|
||||
self.message("Downloading", source)
|
||||
self.message(" ".join(cmd))
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
def download_dir(self, source, dest, relative_dir="."):
|
||||
"""Download a directory from the test machine, recursively.
|
||||
"""
|
||||
assert source and dest
|
||||
assert self.ssh_address
|
||||
|
||||
self._ensure_ssh_master()
|
||||
dest = os.path.join(relative_dir, dest)
|
||||
|
||||
cmd = [
|
||||
"scp", "-B",
|
||||
"-P", str(self.ssh_port),
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "ControlPath=" + self.ssh_master,
|
||||
"-o", "BatchMode=yes",
|
||||
"-r",
|
||||
]
|
||||
if not self.verbose:
|
||||
cmd += ["-q"]
|
||||
cmd += [ "%s@[%s]:%s" % (self.ssh_user, self.ssh_address, source), dest ]
|
||||
|
||||
self.message("Downloading", source)
|
||||
self.message(" ".join(cmd))
|
||||
try:
|
||||
subprocess.check_call(cmd)
|
||||
target = os.path.join(dest, os.path.basename(source))
|
||||
if os.path.exists(target):
|
||||
subprocess.check_call([ "find", target, "-type", "f", "-exec", "chmod", "0644", "{}", ";" ])
|
||||
except:
|
||||
self.message("Error while downloading directory '{0}'".format(source))
|
||||
|
||||
def write(self, dest, content):
|
||||
"""Write a file into the test machine
|
||||
|
||||
Arguments:
|
||||
content: Raw data to write to file
|
||||
dest: The file name in the machine to write to
|
||||
"""
|
||||
assert dest
|
||||
assert self.ssh_address
|
||||
|
||||
cmd = "cat > '%s'" % dest
|
||||
self.execute(command=cmd, input=content)
|
||||
|
||||
def spawn(self, shell_cmd, log_id):
|
||||
"""Spawn a process in the test machine.
|
||||
|
||||
Arguments:
|
||||
shell_cmd: The string to execute by /bin/sh.
|
||||
log_id: The name of the file, realtive to /var/log on the test
|
||||
machine, that will receive stdout and stderr of the command.
|
||||
Returns:
|
||||
The pid of the /bin/sh process that executes the command.
|
||||
"""
|
||||
return int(self.execute("{ (%s) >/var/log/%s 2>&1 & }; echo $!" % (shell_cmd, log_id)))
|
||||
24
bots/machine/machine_core/testvm.py
Normal file
24
bots/machine/machine_core/testvm.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from .timeout import Timeout
|
||||
from .machine import Machine
|
||||
from .exceptions import Failure, RepeatableFailure
|
||||
from .machine_virtual import VirtMachine, VirtNetwork, get_build_image, get_test_image
|
||||
from .constants import BOTS_DIR, TEST_DIR, DEFAULT_IMAGE, TEST_OS_DEFAULT
|
||||
|
||||
__all__ = [Timeout, Machine, Failure, RepeatableFailure, VirtMachine, VirtNetwork, get_build_image, get_test_image, BOTS_DIR, TEST_DIR, DEFAULT_IMAGE, TEST_OS_DEFAULT]
|
||||
52
bots/machine/machine_core/timeout.py
Normal file
52
bots/machine/machine_core/timeout.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import signal
|
||||
|
||||
|
||||
class Timeout:
|
||||
""" Add a timeout to an operation
|
||||
Specify machine to ensure that a machine's ssh operations are canceled when the timer expires.
|
||||
"""
|
||||
def __init__(self, seconds=1, error_message='Timeout', machine=None):
|
||||
if signal.getsignal(signal.SIGALRM) != signal.SIG_DFL:
|
||||
# there is already a different Timeout active
|
||||
self.seconds = None
|
||||
return
|
||||
|
||||
self.seconds = seconds
|
||||
self.error_message = error_message
|
||||
self.machine = machine
|
||||
|
||||
def handle_timeout(self, signum, frame):
|
||||
if self.machine:
|
||||
if self.machine.ssh_process:
|
||||
self.machine.ssh_process.terminate()
|
||||
self.machine.disconnect()
|
||||
|
||||
raise RuntimeError(self.error_message)
|
||||
|
||||
def __enter__(self):
|
||||
if self.seconds:
|
||||
signal.signal(signal.SIGALRM, self.handle_timeout)
|
||||
signal.alarm(self.seconds)
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
if self.seconds:
|
||||
signal.alarm(0)
|
||||
signal.signal(signal.SIGALRM, signal.SIG_DFL)
|
||||
|
||||
76
bots/machine/make-cloud-init-iso
Executable file
76
bots/machine/make-cloud-init-iso
Executable file
|
|
@ -0,0 +1,76 @@
|
|||
#! /bin/bash
|
||||
|
||||
# This file is part of Cockpit.
|
||||
#
|
||||
# Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
set -e
|
||||
|
||||
# create the cloud-init iso
|
||||
init_dir=$(mktemp -d)
|
||||
meta_data="$init_dir/meta-data"
|
||||
user_data="$init_dir/user-data"
|
||||
iso_image="cloud-init.iso"
|
||||
|
||||
vm_user="root"
|
||||
vm_pass="foobar"
|
||||
key_pub=`cat identity.pub`
|
||||
host_key=`sed 's/^/ /' host_key`
|
||||
host_key_pub=`cat host_key.pub`
|
||||
|
||||
mkdir -p $init_dir
|
||||
|
||||
# We don't want to hardcode values:
|
||||
# local-hostname: we want multiple instances of the vm to run in parallel
|
||||
# instance-id: cloud-init skips some init stuff if this is constant (e.g. runcmd)
|
||||
cat >$meta_data <<EOF
|
||||
EOF
|
||||
|
||||
cat >$user_data <<EOF
|
||||
#cloud-config
|
||||
users:
|
||||
- default
|
||||
- name: ${vm_user}
|
||||
groups: users,wheel
|
||||
ssh_authorized_keys:
|
||||
- ${key_pub}
|
||||
- name: admin
|
||||
gecos: Administrator
|
||||
groups: users,wheel
|
||||
ssh_authorized_keys:
|
||||
- ${key_pub}
|
||||
ssh_pwauth: True
|
||||
chpasswd:
|
||||
list: |
|
||||
${vm_user}:${vm_pass}
|
||||
admin:${vm_pass}
|
||||
expire: False
|
||||
ssh_keys:
|
||||
rsa_private: |
|
||||
${host_key}
|
||||
rsa_public: ${host_key_pub}
|
||||
|
||||
# make sure that our user script runs on every boot
|
||||
cloud_final_modules:
|
||||
- scripts-per-once
|
||||
- scripts-per-boot
|
||||
- scripts-per-instance
|
||||
- [scripts-user, always]
|
||||
- final-message
|
||||
|
||||
EOF
|
||||
|
||||
genisoimage -input-charset utf-8 -output $iso_image -volid cidata -joliet -rock $user_data $meta_data
|
||||
44
bots/machine/testvm.py
Executable file
44
bots/machine/testvm.py
Executable file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/python3 -u
|
||||
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# ensure that this module path is present
|
||||
machine_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
if machine_dir not in sys.path:
|
||||
sys.path.insert(1, machine_dir)
|
||||
|
||||
from machine_core.timeout import Timeout
|
||||
from machine_core.machine import Machine
|
||||
from machine_core.exceptions import Failure, RepeatableFailure
|
||||
from machine_core.machine_virtual import VirtMachine, VirtNetwork, get_build_image, get_test_image
|
||||
from machine_core.constants import BOTS_DIR, TEST_DIR, IMAGES_DIR, SCRIPTS_DIR, DEFAULT_IMAGE, TEST_OS_DEFAULT
|
||||
from machine_core.cli import cmd_cli
|
||||
from machine_core.directories import get_images_data_dir, get_temp_dir
|
||||
|
||||
__all__ = [Timeout, Machine, Failure, RepeatableFailure, VirtMachine, VirtNetwork, get_build_image, get_test_image, get_images_data_dir, get_temp_dir, BOTS_DIR, TEST_DIR, IMAGES_DIR, SCRIPTS_DIR, DEFAULT_IMAGE, TEST_OS_DEFAULT]
|
||||
|
||||
# This can be used as helper program for tests not written in Python: Run given
|
||||
# image name until SIGTERM or SIGINT; the image must exist in test/images/;
|
||||
# use image-prepare or image-customize to create that. For example:
|
||||
# $ bots/image-customize -v -i cockpit centos-7
|
||||
# $ bots/machine/testvm.py centos-7
|
||||
if __name__ == "__main__":
|
||||
cmd_cli()
|
||||
Loading…
Add table
Add a link
Reference in a new issue