Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle guest logs #2647

Open
wants to merge 9 commits into
base: refactor-prune
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions tmt/steps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1383,6 +1383,8 @@ class BasePlugin(Phase, Generic[StepDataT, PluginReturnValueT]):

_data_class: type[StepDataT]

preserved_members: list[str] = []

@classmethod
def get_data_class(cls) -> type[StepDataT]:
"""
Expand Down Expand Up @@ -1876,11 +1878,26 @@ def prune(self, logger: tmt.log.Logger) -> None:

if self.workdir is None:
return
logger.debug(f"Remove '{self.name}' workdir '{self.workdir}'.", level=3)
try:
shutil.rmtree(self.workdir)
except OSError as error:
logger.warning(f"Unable to remove '{self.workdir}': {error}")
remove_workdir = True
for member in self.workdir.iterdir():
if member.name in self.preserved_members:
# We only want to remove the workdir, if there is no preserved members
remove_workdir = False
else:
logger.debug(f"Remove '{member}'.", level=3)
try:
if member.is_file() or member.is_symlink():
member.unlink()
else:
shutil.rmtree(member)
except OSError as error:
logger.warning(f"Unable to remove '{member}': {error}")
if remove_workdir:
logger.debug(f"Remove '{self.name}' workdir '{self.workdir}'.", level=3)
try:
shutil.rmtree(self.workdir)
except OSError as error:
logger.warning(f"Unable to remove '{self.workdir}': {error}")


class GuestlessPlugin(BasePlugin[StepDataT, PluginReturnValueT]):
Expand Down
1 change: 1 addition & 0 deletions tmt/steps/finish/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ def go(self, force: bool = False) -> None:

# Stop and remove provisioned guests
for guest in self.plan.provision.guests():
guest.fetch_logs(guest_logs=guest.guest_logs)
guest.stop()
guest.remove()

Expand Down
62 changes: 62 additions & 0 deletions tmt/steps/provision/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,38 @@ def show(
logger.info(key_to_option(key).replace('-', ' '), printable_value, color='green')


@container
class GuestLog:
name: str

def fetch(self) -> str:
"""
Fetch and return content of a log.

:returns: content of the log, or ``None`` if the log cannot be retrieved.
"""
raise NotImplementedError

def store(self, path: Path, logname: Optional[str] = None) -> None:
"""
Save log content to a file.

:param path: a path to save into, could be a directory
or a file path.
:param content: content of the log.
:param logname: name of the log, if not set, logpath
is supposed to be a file path.
"""
# if path is file path
if not path.is_dir():
path.write_text(self.fetch())
# if path is a directory
elif logname:
(path / logname).write_text(self.fetch())
else:
raise tmt.utils.GeneralError('Log path is a directory but log name is not defined.')


class Guest(tmt.utils.Common):
"""
Guest provisioned for test execution
Expand Down Expand Up @@ -1041,6 +1073,7 @@ def __init__(
"""
Initialize guest data
"""
self.guest_logs: list[GuestLog] = []

super().__init__(logger=logger, parent=parent, name=name)
self.load(data)
Expand Down Expand Up @@ -1702,6 +1735,33 @@ def essential_requires(cls) -> list['tmt.base.Dependency']:

return []

@property
def logdir(self) -> Optional[Path]:
"""
Path to store logs
"""
return self.workdir / 'logs' if self.workdir else None

def fetch_logs(
self, dirpath: Optional[Path] = None, guest_logs: Optional[list[GuestLog]] = None
) -> None:
"""
Get log content and save it to a directory.

:param dirpath: a directory to save into. If not set, step's working directory
(:py:attr:`workdir`) or current working directory will be used.
:param lognames: name list of logs need to be handled. If not set, all guest logs
would be collected, as reported by :py:attr:`lognames`.
"""

guest_logs = guest_logs or self.guest_logs or []

dirpath = dirpath or self.logdir or Path.cwd()
if dirpath == self.logdir:
self.logdir.mkdir(parents=True, exist_ok=True)
for log in guest_logs:
log.store(dirpath, log.name)


@container
class GuestSshData(GuestData):
Expand Down Expand Up @@ -2613,6 +2673,8 @@ class ProvisionPlugin(tmt.steps.GuestlessPlugin[ProvisionStepDataT, None]):
# TODO: Generics would provide a better type, https://github.com/teemtee/tmt/issues/1437
_guest: Optional[Guest] = None

preserved_members = ['logs']

@classmethod
def base_command(
cls,
Expand Down
23 changes: 23 additions & 0 deletions tmt/steps/provision/mrack.py
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,14 @@ def get_new_state() -> GuestInspectType:
raise ProvisionError('Failed to create, provisioning failed.')

if state == 'Reserved':
for key in response["logs"]:
self.guest_logs.append(
GuestLogBeaker(key.replace('.log', ''), self, response["logs"][key])
)
# console.log contains dmesg, and accessible even when the system is dead.
self.guest_logs.append(
GuestLogBeaker('dmesg', self, response["logs"]["console.log"])
)
return current

raise tmt.utils.WaitingIncompleteError
Expand Down Expand Up @@ -1474,3 +1482,18 @@ def guest(self) -> Optional[GuestBeaker]:
"""

return self._guest


@container
class GuestLogBeaker(tmt.steps.provision.GuestLog):
guest: GuestBeaker
url: str

def fetch(self) -> str:
"""
Fetch and return content of a log.

:returns: content of the log, or ``None`` if the log cannot be retrieved.
"""

return tmt.utils.get_url_content(self.url) if self.url else ''