blob: 31ef4d2550c34f62c6b900fadded5112a45cd857 [file]
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# -*- coding: utf-8 -*-
"""Utilities to control a guest VM, used for virtual testing with QEMU"""
__author__ = 'Pedro Larroy'
__version__ = '0.1'
import os
import sys
import subprocess
import argparse
import logging
from subprocess import call, check_call, Popen, DEVNULL, PIPE
import time
import sys
import multiprocessing
import shlex
###################################################
#
# Virtual testing with QEMU
#
# We start QEMU instances that have a local port in the host redirected to the ssh port.
#
# The VMs are provisioned after boot, tests are run and then they are stopped
#
QEMU_SSH_PORT=2222
QEMU_RAM=4096
QEMU_RUN="""
qemu-system-arm -M virt -m {ram} \
-kernel vmlinuz \
-initrd initrd.img \
-append 'root=/dev/vda1' \
-drive if=none,file=vda.qcow2,format=qcow2,id=hd \
-device virtio-blk-device,drive=hd \
-netdev user,id=mynet,hostfwd=tcp::{ssh_port}-:22 \
-device virtio-net-device,netdev=mynet \
-display none -nographic
"""
QEMU_RUN_INTERACTIVE="""
qemu-system-arm -M virt -m {ram} \
-kernel vmlinuz \
-initrd initrd.img \
-append 'root=/dev/vda1' \
-drive if=none,file=vda.qcow2,format=qcow2,id=hd \
-device virtio-blk-device,drive=hd \
-netdev user,id=mynet,hostfwd=tcp::{ssh_port}-:22 \
-device virtio-net-device,netdev=mynet \
-nographic
"""
def retry(target_exception, tries=4, delay_s=1, backoff=2):
"""Retry calling the decorated function using an exponential backoff.
http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/
original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry
:param target_exception: the exception to check. may be a tuple of
exceptions to check
:type target_exception: Exception or tuple
:param tries: number of times to try (not retry) before giving up
:type tries: int
:param delay_s: initial delay between retries in seconds
:type delay_s: int
:param backoff: backoff multiplier e.g. value of 2 will double the delay
each retry
:type backoff: int
"""
import time
from functools import wraps
def decorated_retry(f):
@wraps(f)
def f_retry(*args, **kwargs):
mtries, mdelay = tries, delay_s
while mtries > 1:
try:
return f(*args, **kwargs)
except target_exception as e:
logging.warning("Exception: %s, Retrying in %d seconds...", str(e), mdelay)
time.sleep(mdelay)
mtries -= 1
mdelay *= backoff
return f(*args, **kwargs)
return f_retry # true decorator
return decorated_retry
class VMError(RuntimeError):
pass
class VM:
"""Control of the virtual machine"""
def __init__(self, ssh_port=QEMU_SSH_PORT, ram=QEMU_RAM, interactive=False):
self.log = logging.getLogger(VM.__name__)
self.ssh_port = ssh_port
self.timeout_s = 300
self.qemu_process = None
self._detach = False
self._interactive = interactive
self.ram = ram
def __enter__(self):
self.start()
return self
def __exit__(self, exc_type, exc_value, traceback):
if not self._detach:
self.shutdown()
self.terminate()
def start(self):
sys.stderr.flush()
call(['toilet', '-f', 'smbraille', 'Starting QEMU'])
sys.stdout.flush()
self.log.info("Starting VM, ssh port redirected to localhost:%s (inside docker, not exposed by default)", self.ssh_port)
if self.is_running():
raise VMError("VM is running, shutdown first")
if self._interactive:
self.qemu_process = Popen(shlex.split(QEMU_RUN_INTERACTIVE.format(ssh_port=self.ssh_port, ram=self.ram)))
return
else:
self.log.info("Starting in non-interactive mode. Terminal output is disabled.")
self.qemu_process = Popen(shlex.split(QEMU_RUN.format(ssh_port=self.ssh_port, ram=self.ram)), stdout=DEVNULL, stdin=DEVNULL, stderr=PIPE)
def keep_waiting():
return self.is_running()
logging.info("waiting for ssh to be open in the VM (timeout {}s)".format(self.timeout_s))
ssh_working = wait_ssh_open('127.0.0.1', self.ssh_port, keep_waiting, self.timeout_s)
if not self.is_running():
(_, stderr) = self.qemu_process.communicate()
raise VMError("VM failed to start, retcode: {}, stderr: {}".format( self.retcode(), stderr.decode()))
if not ssh_working:
if self.is_running():
self.log.error("VM running but SSH is not working")
self.terminate()
raise VMError("SSH is not working after {} seconds".format(self.timeout_s))
self.log.info("VM is online and SSH is up")
def is_running(self):
return self.qemu_process and self.qemu_process.poll() is None
def retcode(self):
if self.qemu_process:
return self.qemu_process.poll()
else:
raise RuntimeError('qemu process was not started')
def terminate(self):
if self.qemu_process:
logging.info("send term signal")
self.qemu_process.terminate()
time.sleep(3)
logging.info("send kill signal")
self.qemu_process.kill()
self.qemu_process.wait()
self.qemu_process = None
else:
logging.warn("VM.terminate: QEMU process not running")
def detach(self):
self._detach = True
def shutdown(self):
if self.qemu_process:
logging.info("Shutdown via ssh")
# ssh connection will be closed with an error
call(["ssh", "-o", "StrictHostKeyChecking=no", "-p", str(self.ssh_port), "qemu@localhost",
"sudo", "poweroff"])
ret = self.qemu_process.wait(timeout=90)
self.log.info("VM on port %s has shutdown (exit code %d)", self.ssh_port, ret)
self.qemu_process = None
def wait(self):
if self.qemu_process:
self.qemu_process.wait()
def __del__(self):
if self.is_running and not self._detach:
logging.info("VM destructor hit")
self.terminate()
def qemu_ssh(ssh_port=QEMU_SSH_PORT, *args):
check_call(["ssh", "-o", "ServerAliveInterval=5", "-o", "StrictHostKeyChecking=no", "-p{}".format(ssh_port), "qemu@localhost", *args])
def qemu_rsync(ssh_port, local_path, remote_path):
check_call(['rsync', '-e', 'ssh -o StrictHostKeyChecking=no -p{}'.format(ssh_port), '-a', local_path, 'qemu@localhost:{}'.format(remote_path)])
def qemu_rsync_to_host(ssh_port, remote_path, local_path):
check_call(['rsync', '-e', 'ssh -o StrictHostKeyChecking=no -p{}'.format(ssh_port), '-va', 'qemu@localhost:{}'.format(remote_path), local_path])
@retry(subprocess.CalledProcessError)
def qemu_provision(ssh_port=QEMU_SSH_PORT):
import glob
logging.info("Provisioning the VM with artifacts and sources")
artifact = glob.glob('/work/mxnet/build/*.whl')
for x in artifact:
qemu_rsync(ssh_port, x, 'mxnet_dist/')
qemu_rsync(ssh_port, '/work/runtime_functions.py','')
qemu_rsync(ssh_port, '/work/vmcontrol.py','')
qemu_rsync(ssh_port, 'mxnet/tests', 'mxnet')
qemu_rsync(ssh_port, 'mxnet/ci/qemu/test_requirements.txt', 'mxnet/test_requirements.txt')
logging.info("Provisioning completed successfully.")
def wait_ssh_open(server, port, keep_waiting=None, timeout=None):
""" Wait for network service to appear
@param server: host to connect to (str)
@param port: port (int)
@param timeout: in seconds, if None or 0 wait forever
@return: True of False, if timeout is None may return only True or
throw unhandled network exception
"""
import socket
import errno
import time
log = logging.getLogger('wait_ssh_open')
sleep_s = 1
if timeout:
from time import time as now
# time module is needed to calc timeout shared between two exceptions
end = now() + timeout
while True:
log.debug("Sleeping for %s second(s)", sleep_s)
time.sleep(sleep_s)
s = socket.socket()
try:
if keep_waiting and not keep_waiting():
log.debug("keep_waiting() is set and evaluates to False")
return False
if timeout:
next_timeout = end - now()
if next_timeout < 0:
log.debug("connect time out")
return False
else:
log.debug("connect timeout %d s", next_timeout)
s.settimeout(next_timeout)
log.debug("connect %s:%d", server, port)
s.connect((server, port))
ret = s.recv(1024).decode()
if ret and ret.startswith('SSH'):
s.close()
log.info("wait_ssh_open: port %s:%s is open and ssh is ready", server, port)
return True
else:
log.debug("Didn't get the SSH banner")
s.close()
except ConnectionError as err:
log.debug("ConnectionError %s", err)
if sleep_s == 0:
sleep_s = 1
else:
sleep_s *= 2
except socket.gaierror as err:
log.debug("gaierror %s",err)
return False
except socket.timeout as err:
# this exception occurs only if timeout is set
if timeout:
return False
except TimeoutError as err:
# catch timeout exception from underlying network library
# this one is different from socket.timeout
raise
def wait_port_open(server, port, timeout=None):
""" Wait for network service to appear
@param server: host to connect to (str)
@param port: port (int)
@param timeout: in seconds, if None or 0 wait forever
@return: True of False, if timeout is None may return only True or
throw unhandled network exception
"""
import socket
import errno
import time
sleep_s = 0
if timeout:
from time import time as now
# time module is needed to calc timeout shared between two exceptions
end = now() + timeout
while True:
logging.debug("Sleeping for %s second(s)", sleep_s)
time.sleep(sleep_s)
s = socket.socket()
try:
if timeout:
next_timeout = end - now()
if next_timeout < 0:
return False
else:
s.settimeout(next_timeout)
logging.info("connect %s %d", server, port)
s.connect((server, port))
except ConnectionError as err:
logging.debug("ConnectionError %s", err)
if sleep_s == 0:
sleep_s = 1
except socket.gaierror as err:
logging.debug("gaierror %s",err)
return False
except socket.timeout as err:
# this exception occurs only if timeout is set
if timeout:
return False
except TimeoutError as err:
# catch timeout exception from underlying network library
# this one is different from socket.timeout
raise
else:
s.close()
logging.info("wait_port_open: port %s:%s is open", server, port)
return True