Skip to content
Snippets Groups Projects
Verified Commit ffcf0b74 authored by Karel Koci's avatar Karel Koci :metal:
Browse files

nsfarm: improve and clean code

parent ab6ef7df
Branches
1 merge request!2Initial development
......@@ -6,7 +6,6 @@ Never add different scope to these fixtures. Keep it always as "function" (defau
"""
import pytest
import os
import argparse
import configparser
pytest_plugins = ("pytest_html", )
......
"""CLI comunication helper classes based on pexpect.
"""CLI communication helper classes based on pexpect.
This ensures systematic logging and access to terminals. We implement two special terminal types at the moment. We have
support for shell and u-boot. They differ in a way how they handle prompt and methods they provide to user.
"""
# Notest on some of the hacks in this file:
#
# There are new line character matches in regular expressions. Correct one is \r\n but some serial controlles for some
# reason also use \n\r so we match both alternatives.
#
import logging
import base64
_FLUSH_BUFFLEN = 2048
def pexpect_flush(pexpect_handle):
"""Flush all input on pexpect. This effectively reads everything.
"""
# TODO fix: this timeouts if there is nothing to flush
bufflen = 2048
while len(pexpect_handle.read_nonblocking(bufflen)) == bufflen:
while len(pexpect_handle.read_nonblocking(_FLUSH_BUFFLEN)) == _FLUSH_BUFFLEN:
pass
......@@ -46,7 +52,7 @@ class Cli:
def command(self, cmd=""):
"""Calls pexpect sendline and expect cmd with trailing new line.
This is handy when you are comunicating with console that echoes input back. This effectively removes sent
This is handy when you are communicating with console that echoes input back. This effectively removes sent
command from output.
"""
self.sendline(cmd)
......@@ -55,21 +61,24 @@ class Cli:
self.expect_exact(cmd)
self.expect_exact(["\r\n", "\n\r"])
def run(self, cmd="", exit_code=lambda ec: ec == 0, **kwargs):
@staticmethod
def _run_exit_code_zero(exit_code):
if exit_code != 0:
raise Exception("Command exited with non-zero code: {}".format(exit_code))
def run(self, cmd="", exit_code=_run_exit_code_zero, **kwargs):
"""Run given command and follow output until prompt is reached and return exit code with optional automatic
check. This is same as if you would call cmd() and prompt() witch asserting exit_code.
check. This is same as if you would call cmd() and prompt() while checking exit_code.
cmd: command to be executed
exit_code: lambda function verifying exit code or None to skip assert
exit_code: lambda function verifying exit code or None to skip default check
All other key-word arguments are passed to prompt call.
Returns exit code of command.
Returns result of exit_code function or exit code of command if exit_code is None.
"""
self.command(cmd)
ec = self.prompt(**kwargs)
if exit_code is not None:
assert exit_code(ec)
return exit_code
ecode = self.prompt(**kwargs)
return ecode if exit_code is None else exit_code(ecode)
def match(self, index):
"""Returns located match in previously matched output.
......@@ -121,7 +130,7 @@ class Shell(Cli):
This returns bytes with content of file from path.
"""
self_sh.run("base64 '{}'".format(path))
self._sh.run("base64 '{}'".format(path))
return base64.b64decode(self._sh.output())
def file_write(self, path, content):
......@@ -133,7 +142,8 @@ class Shell(Cli):
self._sh.sendline("base64 --decode > '{}'".format(path))
self._sh.sendline(base64.b64encode(content))
self._sh.sendeof()
assert self._sh.prompt() == 0
if self._sh.prompt() != 0:
raise Exception("Writing file failed with exit code: {}".format(self._sh.prompt()))
@property
def output(self):
......@@ -198,8 +208,3 @@ class PexpectLogging:
"""Standard-like flush function.
"""
# Just ignore flush
# Notest on some of the hacks in this file
#
# There are new line character matches in regular expressions. Correct one is \r\n but some serial controlles for some
# reason also use \n\r so we match both alternatives.
......@@ -10,8 +10,8 @@ IMAGES = None
LOCAL = None
def _profile_device(profile, checkfunc):
return True not in {checkfunc(dev) for _, dev in profile.devices.items()}
def _profile_device(profile, check_func):
return True not in {check_func(dev) for _, dev in profile.devices.items()}
def connect():
......
......@@ -15,7 +15,7 @@ IMGS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__
LOGGER = logging.getLogger(__package__)
class Container():
class Container:
"""Generic container handle.
"""
# TODO log syslog somehow
......@@ -24,18 +24,18 @@ class Container():
self._name = img_name
self._internet = internet
self._devices = tuple(devices)
self._dpath = os.path.join(IMGS_DIR, img_name)
self._fpath = self._dpath + ".sh"
self._dir_path = os.path.join(IMGS_DIR, img_name)
self._file_path = self._dir_path + ".sh"
self._logger = logging.getLogger("{}[{}]".format(__package__, img_name))
# Verify existence of image definition
if not os.path.isfile(self._fpath):
raise Exception("There seems to be no file describing image: {}".format(self._fpath))
if not os.path.isdir(self._dpath):
self._dpath = None
if not os.path.isfile(self._file_path):
raise Exception("There seems to be no file describing image: {}".format(self._file_path))
if not os.path.isdir(self._dir_path):
self._dir_path = None
# Make sure that we are connected to LXD
_lxd.connect()
# Get parent
with open(self._fpath) as file:
with open(self._file_path) as file:
# This reads second line of file while initial hash removed
parent = next(itertools.islice(file, 1, 2))[1:].strip()
self._parent = None
......@@ -60,14 +60,14 @@ class Container():
else:
md5sum.update(self._parent.fingerprint.encode())
# File defining container
with open(self._fpath, "rb") as file:
with open(self._file_path, "rb") as file:
md5sum.update(file.read())
# Additional nodes from directory
if self._dpath:
nodes = os.listdir(self._dpath)
if self._dir_path:
nodes = os.listdir(self._dir_path)
while nodes:
node = nodes.pop()
path = os.path.join(self._dpath, node)
path = os.path.join(self._dir_path, node)
md5sum.update(node.encode())
if os.path.isdir(path):
nodes += [os.path.join(node, nd) for nd in os.listdir(path)]
......@@ -80,18 +80,6 @@ class Container():
md5sum.update(os.readlink(path).encode())
return md5sum.hexdigest()
def _container_name(self, prefix="nsfarm"):
name = "{}-{}-{}".format(
prefix,
self._name,
os.getpid())
if _lxd.LOCAL.containers.exists(name):
i = 1
while _lxd.LOCAL.containers.exists("{}-{}".format(name, i)):
i += 1
name = "{}-{}".format(name, i)
return name
def prepare_image(self):
"""Prepare image for this container if not already prepared.
......@@ -137,10 +125,10 @@ class Container():
try:
# TODO log boostrap process
# Copy script and files to container
with open(self._fpath) as file:
with open(self._file_path) as file:
container.files.put(IMAGE_INIT_PATH, file.read(), mode=700)
if self._dpath:
container.files.recursive_put(self._dpath, "/")
if self._dir_path:
container.files.recursive_put(self._dir_path, "/")
# Run script to bootstrap image
container.start(wait=True)
try:
......@@ -187,6 +175,15 @@ class Container():
# TODO we could somehow just let it create it and return from this method and wait later on when we realy need
# container.
def _container_name(self, prefix="nsfarm"):
name = "{}-{}-{}".format(prefix, self._name, os.getpid())
if _lxd.LOCAL.containers.exists(name):
i = 1
while _lxd.LOCAL.containers.exists("{}-{}".format(name, i)):
i += 1
name = "{}-{}".format(name, i)
return name
def cleanup(self):
"""Remove container if it exists.
......@@ -205,7 +202,7 @@ class Container():
self._lxd_container.stop()
self._lxd_container = None
def pexpect(self, command=["/bin/sh"]):
def pexpect(self, command=("/bin/sh",)):
"""Returns pexpect handle for command running in container.
"""
assert self._lxd_container is not None
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment