diff --git a/conftest.py b/conftest.py index c5463aa4fee678ae481199367f63e328ce84f1ea..1f12df0385ba72e2817362e6b9b10fe57d14ed39 100644 --- a/conftest.py +++ b/conftest.py @@ -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", ) diff --git a/nsfarm/cli.py b/nsfarm/cli.py index c1fdc30d5d92c4f5bf347ec364cfbd54a66ac672..5fa492beb0763224b56154e5094e7dbcd2f6c1c8 100644 --- a/nsfarm/cli.py +++ b/nsfarm/cli.py @@ -1,18 +1,24 @@ -"""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. diff --git a/nsfarm/lxd/_lxd.py b/nsfarm/lxd/_lxd.py index 38e0a826e898b2df087983541a419b914ef0ba94..57316bc6c8e4e36a9aac329664edbc2b516c30af 100644 --- a/nsfarm/lxd/_lxd.py +++ b/nsfarm/lxd/_lxd.py @@ -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(): diff --git a/nsfarm/lxd/container.py b/nsfarm/lxd/container.py index 8b100d6b7ef0a8e1362c5164d792fb56e10bb97d..06c08ff4f079ef5a2afd7b04103441c84f16d394 100644 --- a/nsfarm/lxd/container.py +++ b/nsfarm/lxd/container.py @@ -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