From 424427825faada9a0571a4e90b9728da4263a5d8 Mon Sep 17 00:00:00 2001 From: Lili Nie Date: Tue, 25 Feb 2025 16:59:53 +0800 Subject: [PATCH 1/9] Do not remove plugin's workdir if there are preserved members --- tmt/steps/__init__.py | 27 ++++++++++++++++++++++----- tmt/steps/provision/__init__.py | 2 ++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/tmt/steps/__init__.py b/tmt/steps/__init__.py index d19fe3cad7..b8c27fce14 100644 --- a/tmt/steps/__init__.py +++ b/tmt/steps/__init__.py @@ -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]: """ @@ -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]): diff --git a/tmt/steps/provision/__init__.py b/tmt/steps/provision/__init__.py index 20e0dc0b0c..871977799e 100644 --- a/tmt/steps/provision/__init__.py +++ b/tmt/steps/provision/__init__.py @@ -2613,6 +2613,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, From cc4f1e82bc7028d0a28a3f4f90189d18fff6d922 Mon Sep 17 00:00:00 2001 From: Lili Nie Date: Fri, 26 Jan 2024 15:54:17 +0800 Subject: [PATCH 2/9] Handle guest logs Fetch log content,save it to provided location or current dir --- tmt/steps/finish/__init__.py | 1 + tmt/steps/provision/__init__.py | 41 ++++++++++++++++++++++++++++++++ tmt/steps/provision/artemis.py | 4 ++++ tmt/steps/provision/connect.py | 4 ++++ tmt/steps/provision/local.py | 10 ++++++++ tmt/steps/provision/mrack.py | 4 ++++ tmt/steps/provision/podman.py | 4 ++++ tmt/steps/provision/testcloud.py | 4 ++++ 8 files changed, 72 insertions(+) diff --git a/tmt/steps/finish/__init__.py b/tmt/steps/finish/__init__.py index 55ed62910d..6e862ad11b 100644 --- a/tmt/steps/finish/__init__.py +++ b/tmt/steps/finish/__init__.py @@ -226,6 +226,7 @@ def go(self, force: bool = False) -> None: # Stop and remove provisioned guests for guest in self.plan.provision.guests(): + guest.handle_guest_logs(logs=guest.logs) guest.stop() guest.remove() diff --git a/tmt/steps/provision/__init__.py b/tmt/steps/provision/__init__.py index 871977799e..4bc6ac9dba 100644 --- a/tmt/steps/provision/__init__.py +++ b/tmt/steps/provision/__init__.py @@ -1113,6 +1113,11 @@ def scripts_path(self) -> Path: else tmt.steps.execute.DEFAULT_SCRIPTS_DEST_DIR ) + @property + def logs(self) -> list[str]: + + raise NotImplementedError + @classmethod def options(cls, how: Optional[str] = None) -> list[tmt.options.ClickOptionDecoratorType]: """ @@ -1702,6 +1707,42 @@ def essential_requires(cls) -> list['tmt.base.Dependency']: return [] + def acquire_log(self, log_name: str) -> Optional[str]: + """fetch and return content of a requested log""" + raise NotImplementedError + + def store_log( + self, + log_path: Path, + log_content: str, + log_name: Optional[str] = None) -> None: + """save log content and return the path""" + if log_name: + # if log_path contains log file name + if log_path.is_dir(): + log_path.write_text(log_content) + else: + (log_path / log_name).write_text(log_content) + else: + log_path.write_text(log_content) + + def handle_guest_logs(self, + log_path: Optional[Path] = None, + logs: Optional[list[str]] = None) -> None: + """get log content and save it to the workdir/provision/, + list the log dir in guests.yaml""" + logs = logs or [] + log_path = log_path or self.workdir + for log_name in logs: + log_content = self.acquire_log(log_name) + if log_content: + if log_path: + self.store_log(log_path, log_content, log_name) + else: + self.store_log(Path.cwd(), log_content, log_name) + else: + self.debug(f'no content in {log_name}') + @container class GuestSshData(GuestData): diff --git a/tmt/steps/provision/artemis.py b/tmt/steps/provision/artemis.py index 53adb7ea2f..94a12e7649 100644 --- a/tmt/steps/provision/artemis.py +++ b/tmt/steps/provision/artemis.py @@ -493,6 +493,10 @@ def is_ready(self) -> bool: # return True if self.guest is not None return self.primary_address is not None + @property + def logs(self) -> list[str]: + return [] + def _create(self) -> None: environment: dict[str, Any] = { 'hw': {'arch': self.arch}, diff --git a/tmt/steps/provision/connect.py b/tmt/steps/provision/connect.py index c9b330943a..9bd2a7656a 100644 --- a/tmt/steps/provision/connect.py +++ b/tmt/steps/provision/connect.py @@ -85,6 +85,10 @@ class GuestConnect(tmt.steps.provision.GuestSsh): soft_reboot: Optional[ShellScript] hard_reboot: Optional[ShellScript] + @property + def logs(self) -> list[str]: + return [] + def reboot( self, hard: bool = False, diff --git a/tmt/steps/provision/local.py b/tmt/steps/provision/local.py index 4e043b1cd8..0d63349ea5 100644 --- a/tmt/steps/provision/local.py +++ b/tmt/steps/provision/local.py @@ -32,6 +32,10 @@ def is_ready(self) -> bool: return True + @property + def logs(self) -> list[str]: + return ['dmesg'] + def _run_ansible( self, playbook: tmt.steps.provision.AnsibleApplicable, @@ -179,6 +183,12 @@ def pull( Nothing to be done to pull workdir """ + def acquire_log(self, log_name: str) -> Optional[str]: + """fetch and return content of a requested log""" + if log_name == 'dmesg': + return self.execute(Command('dmesg')).stdout + return None + @tmt.steps.provides_method('local') class ProvisionLocal(tmt.steps.provision.ProvisionPlugin[ProvisionLocalData]): diff --git a/tmt/steps/provision/mrack.py b/tmt/steps/provision/mrack.py index 95c145679b..1d2486729d 100644 --- a/tmt/steps/provision/mrack.py +++ b/tmt/steps/provision/mrack.py @@ -1186,6 +1186,10 @@ def is_ready(self) -> bool: except mrack.errors.MrackError: return False + @property + def logs(self) -> list[str]: + return [] + def _create(self, tmt_name: str) -> None: """ Create beaker job xml request and submit it to Beaker hub diff --git a/tmt/steps/provision/podman.py b/tmt/steps/provision/podman.py index 2869c346af..642aed07b1 100644 --- a/tmt/steps/provision/podman.py +++ b/tmt/steps/provision/podman.py @@ -128,6 +128,10 @@ def is_ready(self) -> bool: ) return str(cmd_output.stdout).strip() == 'true' + @property + def logs(self) -> list[str]: + return [] + def wake(self) -> None: """ Wake up the guest diff --git a/tmt/steps/provision/testcloud.py b/tmt/steps/provision/testcloud.py index b83da3a8f0..63e4c011c7 100644 --- a/tmt/steps/provision/testcloud.py +++ b/tmt/steps/provision/testcloud.py @@ -715,6 +715,10 @@ def is_coreos(self) -> bool: # Is this a CoreOS? return bool(re.search('coreos|rhcos', self.image.lower())) + @property + def logs(self) -> list[str]: + return [] + def _get_url(self, url: str, message: str) -> requests.Response: """ Get url, retry when fails, return response From 409b707bda52ae483ea394e6aa118472f8aefdda Mon Sep 17 00:00:00 2001 From: Lili Nie Date: Fri, 2 Feb 2024 11:13:03 -0500 Subject: [PATCH 3/9] Handle guest logs --- tmt/steps/finish/__init__.py | 2 +- tmt/steps/provision/__init__.py | 60 +++++++++++++++++++++----------- tmt/steps/provision/artemis.py | 3 +- tmt/steps/provision/connect.py | 3 +- tmt/steps/provision/local.py | 9 +++-- tmt/steps/provision/mrack.py | 3 +- tmt/steps/provision/podman.py | 3 +- tmt/steps/provision/testcloud.py | 3 +- 8 files changed, 57 insertions(+), 29 deletions(-) diff --git a/tmt/steps/finish/__init__.py b/tmt/steps/finish/__init__.py index 6e862ad11b..ce68d0e3e6 100644 --- a/tmt/steps/finish/__init__.py +++ b/tmt/steps/finish/__init__.py @@ -226,7 +226,7 @@ def go(self, force: bool = False) -> None: # Stop and remove provisioned guests for guest in self.plan.provision.guests(): - guest.handle_guest_logs(logs=guest.logs) + guest.handle_guest_logs(log_names=guest.log_names) guest.stop() guest.remove() diff --git a/tmt/steps/provision/__init__.py b/tmt/steps/provision/__init__.py index 4bc6ac9dba..fc05e77c16 100644 --- a/tmt/steps/provision/__init__.py +++ b/tmt/steps/provision/__init__.py @@ -1114,9 +1114,10 @@ def scripts_path(self) -> Path: ) @property - def logs(self) -> list[str]: + def log_names(self) -> list[str]: + """Return name list of logs the guest could provide.""" - raise NotImplementedError + return [] @classmethod def options(cls, how: Optional[str] = None) -> list[tmt.options.ClickOptionDecoratorType]: @@ -1708,7 +1709,11 @@ def essential_requires(cls) -> list['tmt.base.Dependency']: return [] def acquire_log(self, log_name: str) -> Optional[str]: - """fetch and return content of a requested log""" + """ + Fetch and return content of a log. + :param log_name: name of the log. + :returns: content of the log. + """ raise NotImplementedError def store_log( @@ -1716,30 +1721,43 @@ def store_log( log_path: Path, log_content: str, log_name: Optional[str] = None) -> None: - """save log content and return the path""" - if log_name: - # if log_path contains log file name - if log_path.is_dir(): - log_path.write_text(log_content) - else: - (log_path / log_name).write_text(log_content) - else: + """ + Save log content to a file. + :param log_path: a path to save into,could be a directory + or a file path. + :param log_content: content of the log. + :param log_name: name of the log, if not set, log_path + is supposed to be '/dev/null' or a file path. + """ + # if log_path is file path or /dev/null + if not log_path.is_dir() or str(log_path) == '/dev/null': log_path.write_text(log_content) + # log_path is a directory + else: + if log_name: + name_str = log_name + else: + name_str = 'tmt-guestlog-' + \ + datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d%H%M%S") + self.warn(f'log_name is not set, using file name:{name_str}') + (log_path / name_str).write_text(log_content) def handle_guest_logs(self, log_path: Optional[Path] = None, - logs: Optional[list[str]] = None) -> None: - """get log content and save it to the workdir/provision/, - list the log dir in guests.yaml""" - logs = logs or [] - log_path = log_path or self.workdir - for log_name in logs: + log_names: Optional[list[str]] = None) -> None: + """ + Get log content and save it to a directory. + :param log_path: a directory to save into.If not set,self.workdir + or Path.cwd() will be used. + :param log_names: name list of logs need to be handled.If not set, + self.log_names will be used. + """ + log_names = log_names or self.log_names + log_path = log_path or self.workdir or Path.cwd() + for log_name in log_names: log_content = self.acquire_log(log_name) if log_content: - if log_path: - self.store_log(log_path, log_content, log_name) - else: - self.store_log(Path.cwd(), log_content, log_name) + self.store_log(log_path, log_content, log_name) else: self.debug(f'no content in {log_name}') diff --git a/tmt/steps/provision/artemis.py b/tmt/steps/provision/artemis.py index 94a12e7649..291d5791fb 100644 --- a/tmt/steps/provision/artemis.py +++ b/tmt/steps/provision/artemis.py @@ -494,7 +494,8 @@ def is_ready(self) -> bool: return self.primary_address is not None @property - def logs(self) -> list[str]: + def log_names(self) -> list[str]: + """Return name list of logs the guest could provide.""" return [] def _create(self) -> None: diff --git a/tmt/steps/provision/connect.py b/tmt/steps/provision/connect.py index 9bd2a7656a..c52a7e6a65 100644 --- a/tmt/steps/provision/connect.py +++ b/tmt/steps/provision/connect.py @@ -86,7 +86,8 @@ class GuestConnect(tmt.steps.provision.GuestSsh): hard_reboot: Optional[ShellScript] @property - def logs(self) -> list[str]: + def log_names(self) -> list[str]: + """Return name list of logs the guest could provide.""" return [] def reboot( diff --git a/tmt/steps/provision/local.py b/tmt/steps/provision/local.py index 0d63349ea5..35e67ff08f 100644 --- a/tmt/steps/provision/local.py +++ b/tmt/steps/provision/local.py @@ -33,7 +33,8 @@ def is_ready(self) -> bool: return True @property - def logs(self) -> list[str]: + def log_names(self) -> list[str]: + """Return name list of logs the guest could provide.""" return ['dmesg'] def _run_ansible( @@ -184,7 +185,11 @@ def pull( """ def acquire_log(self, log_name: str) -> Optional[str]: - """fetch and return content of a requested log""" + """ + Fetch and return content of a log. + :param log_name: name of the log. + :returns: content of the log. + """ if log_name == 'dmesg': return self.execute(Command('dmesg')).stdout return None diff --git a/tmt/steps/provision/mrack.py b/tmt/steps/provision/mrack.py index 1d2486729d..41e76b062e 100644 --- a/tmt/steps/provision/mrack.py +++ b/tmt/steps/provision/mrack.py @@ -1187,7 +1187,8 @@ def is_ready(self) -> bool: return False @property - def logs(self) -> list[str]: + def log_names(self) -> list[str]: + """Return name list of logs the guest could provide.""" return [] def _create(self, tmt_name: str) -> None: diff --git a/tmt/steps/provision/podman.py b/tmt/steps/provision/podman.py index 642aed07b1..5df19c171b 100644 --- a/tmt/steps/provision/podman.py +++ b/tmt/steps/provision/podman.py @@ -129,7 +129,8 @@ def is_ready(self) -> bool: return str(cmd_output.stdout).strip() == 'true' @property - def logs(self) -> list[str]: + def log_names(self) -> list[str]: + """Return name list of logs the guest could provide.""" return [] def wake(self) -> None: diff --git a/tmt/steps/provision/testcloud.py b/tmt/steps/provision/testcloud.py index 63e4c011c7..860d7345d3 100644 --- a/tmt/steps/provision/testcloud.py +++ b/tmt/steps/provision/testcloud.py @@ -716,7 +716,8 @@ def is_coreos(self) -> bool: return bool(re.search('coreos|rhcos', self.image.lower())) @property - def logs(self) -> list[str]: + def log_names(self) -> list[str]: + """Return name list of logs the guest could provide.""" return [] def _get_url(self, url: str, message: str) -> requests.Response: From 9df86d0eabb4280adf45285f80f7c210344adf37 Mon Sep 17 00:00:00 2001 From: Lili Nie Date: Sun, 4 Feb 2024 04:12:45 -0500 Subject: [PATCH 4/9] handle guest --- tmt/steps/provision/__init__.py | 26 +++++++++++++------------- tmt/steps/provision/artemis.py | 2 +- tmt/steps/provision/connect.py | 2 +- tmt/steps/provision/local.py | 5 +++-- tmt/steps/provision/mrack.py | 2 +- tmt/steps/provision/podman.py | 2 +- tmt/steps/provision/testcloud.py | 2 +- 7 files changed, 21 insertions(+), 20 deletions(-) diff --git a/tmt/steps/provision/__init__.py b/tmt/steps/provision/__init__.py index fc05e77c16..e5f844a521 100644 --- a/tmt/steps/provision/__init__.py +++ b/tmt/steps/provision/__init__.py @@ -1115,7 +1115,7 @@ def scripts_path(self) -> Path: @property def log_names(self) -> list[str]: - """Return name list of logs the guest could provide.""" + """ Return name list of logs the guest could provide. """ return [] @@ -1711,8 +1711,9 @@ def essential_requires(cls) -> list['tmt.base.Dependency']: def acquire_log(self, log_name: str) -> Optional[str]: """ Fetch and return content of a log. + :param log_name: name of the log. - :returns: content of the log. + :returns: content of the log, or None if the log cannot be retrieved. """ raise NotImplementedError @@ -1723,34 +1724,33 @@ def store_log( log_name: Optional[str] = None) -> None: """ Save log content to a file. + :param log_path: a path to save into,could be a directory - or a file path. + or a file path. :param log_content: content of the log. :param log_name: name of the log, if not set, log_path - is supposed to be '/dev/null' or a file path. + is supposed to be a file path. """ - # if log_path is file path or /dev/null - if not log_path.is_dir() or str(log_path) == '/dev/null': + # if log_path is file path + if not log_path.is_dir(): log_path.write_text(log_content) # log_path is a directory else: if log_name: - name_str = log_name + (log_path / log_name).write_text(log_content) else: - name_str = 'tmt-guestlog-' + \ - datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d%H%M%S") - self.warn(f'log_name is not set, using file name:{name_str}') - (log_path / name_str).write_text(log_content) + raise tmt.utils.GeneralError('log_name is None.') def handle_guest_logs(self, log_path: Optional[Path] = None, log_names: Optional[list[str]] = None) -> None: """ Get log content and save it to a directory. + :param log_path: a directory to save into.If not set,self.workdir - or Path.cwd() will be used. + or Path.cwd() will be used. :param log_names: name list of logs need to be handled.If not set, - self.log_names will be used. + self.log_names will be used. """ log_names = log_names or self.log_names log_path = log_path or self.workdir or Path.cwd() diff --git a/tmt/steps/provision/artemis.py b/tmt/steps/provision/artemis.py index 291d5791fb..c6525f1821 100644 --- a/tmt/steps/provision/artemis.py +++ b/tmt/steps/provision/artemis.py @@ -495,7 +495,7 @@ def is_ready(self) -> bool: @property def log_names(self) -> list[str]: - """Return name list of logs the guest could provide.""" + """ Return name list of logs the guest could provide. """ return [] def _create(self) -> None: diff --git a/tmt/steps/provision/connect.py b/tmt/steps/provision/connect.py index c52a7e6a65..cf20298d9d 100644 --- a/tmt/steps/provision/connect.py +++ b/tmt/steps/provision/connect.py @@ -87,7 +87,7 @@ class GuestConnect(tmt.steps.provision.GuestSsh): @property def log_names(self) -> list[str]: - """Return name list of logs the guest could provide.""" + """ Return name list of logs the guest could provide. """ return [] def reboot( diff --git a/tmt/steps/provision/local.py b/tmt/steps/provision/local.py index 35e67ff08f..6eb5156a5f 100644 --- a/tmt/steps/provision/local.py +++ b/tmt/steps/provision/local.py @@ -34,7 +34,7 @@ def is_ready(self) -> bool: @property def log_names(self) -> list[str]: - """Return name list of logs the guest could provide.""" + """ Return name list of logs the guest could provide. """ return ['dmesg'] def _run_ansible( @@ -187,8 +187,9 @@ def pull( def acquire_log(self, log_name: str) -> Optional[str]: """ Fetch and return content of a log. + :param log_name: name of the log. - :returns: content of the log. + :returns: content of the log, or None if the log cannot be retrieved. """ if log_name == 'dmesg': return self.execute(Command('dmesg')).stdout diff --git a/tmt/steps/provision/mrack.py b/tmt/steps/provision/mrack.py index 41e76b062e..b81b64160b 100644 --- a/tmt/steps/provision/mrack.py +++ b/tmt/steps/provision/mrack.py @@ -1188,7 +1188,7 @@ def is_ready(self) -> bool: @property def log_names(self) -> list[str]: - """Return name list of logs the guest could provide.""" + """ Return name list of logs the guest could provide. """ return [] def _create(self, tmt_name: str) -> None: diff --git a/tmt/steps/provision/podman.py b/tmt/steps/provision/podman.py index 5df19c171b..95c7450790 100644 --- a/tmt/steps/provision/podman.py +++ b/tmt/steps/provision/podman.py @@ -130,7 +130,7 @@ def is_ready(self) -> bool: @property def log_names(self) -> list[str]: - """Return name list of logs the guest could provide.""" + """ Return name list of logs the guest could provide. """ return [] def wake(self) -> None: diff --git a/tmt/steps/provision/testcloud.py b/tmt/steps/provision/testcloud.py index 860d7345d3..fd99ac242e 100644 --- a/tmt/steps/provision/testcloud.py +++ b/tmt/steps/provision/testcloud.py @@ -717,7 +717,7 @@ def is_coreos(self) -> bool: @property def log_names(self) -> list[str]: - """Return name list of logs the guest could provide.""" + """ Return name list of logs the guest could provide. """ return [] def _get_url(self, url: str, message: str) -> requests.Response: From cf14b7e314c7bd6669a27bd4684933c17d389d45 Mon Sep 17 00:00:00 2001 From: Lili Nie Date: Wed, 7 Feb 2024 19:50:51 -0500 Subject: [PATCH 5/9] Handle guest logs --- tmt/steps/provision/__init__.py | 20 ++++++++++---------- tmt/steps/provision/local.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tmt/steps/provision/__init__.py b/tmt/steps/provision/__init__.py index e5f844a521..a3d2091f16 100644 --- a/tmt/steps/provision/__init__.py +++ b/tmt/steps/provision/__init__.py @@ -1713,7 +1713,7 @@ def acquire_log(self, log_name: str) -> Optional[str]: Fetch and return content of a log. :param log_name: name of the log. - :returns: content of the log, or None if the log cannot be retrieved. + :returns: content of the log, or ``None`` if the log cannot be retrieved. """ raise NotImplementedError @@ -1725,7 +1725,7 @@ def store_log( """ Save log content to a file. - :param log_path: a path to save into,could be a directory + :param log_path: a path to save into, could be a directory or a file path. :param log_content: content of the log. :param log_name: name of the log, if not set, log_path @@ -1735,11 +1735,11 @@ def store_log( if not log_path.is_dir(): log_path.write_text(log_content) # log_path is a directory + elif log_name: + (log_path / log_name).write_text(log_content) else: - if log_name: - (log_path / log_name).write_text(log_content) - else: - raise tmt.utils.GeneralError('log_name is None.') + raise tmt.utils.GeneralError( + 'Log path is a directory but log name is not defined.') def handle_guest_logs(self, log_path: Optional[Path] = None, @@ -1747,10 +1747,10 @@ def handle_guest_logs(self, """ Get log content and save it to a directory. - :param log_path: a directory to save into.If not set,self.workdir - or Path.cwd() will be used. - :param log_names: name list of logs need to be handled.If not set, - self.log_names will be used. + :param log_path: a directory to save into. If not set, step's working directory + (:py:attr:`workdir`) or current working directory will be used. + :param log_names: name list of logs need to be handled. If not set, all guest logs + would be collected, as reported by :py:attr:`log_names`. """ log_names = log_names or self.log_names log_path = log_path or self.workdir or Path.cwd() diff --git a/tmt/steps/provision/local.py b/tmt/steps/provision/local.py index 6eb5156a5f..a060c54f04 100644 --- a/tmt/steps/provision/local.py +++ b/tmt/steps/provision/local.py @@ -189,7 +189,7 @@ def acquire_log(self, log_name: str) -> Optional[str]: Fetch and return content of a log. :param log_name: name of the log. - :returns: content of the log, or None if the log cannot be retrieved. + :returns: content of the log, or ``None`` if the log cannot be retrieved. """ if log_name == 'dmesg': return self.execute(Command('dmesg')).stdout From 187f1160416bd01981316fa55db9f98f8fef97ad Mon Sep 17 00:00:00 2001 From: lnie Date: Mon, 6 Jan 2025 09:47:50 -0500 Subject: [PATCH 6/9] squash:update --- tmt/steps/finish/__init__.py | 2 +- tmt/steps/provision/__init__.py | 58 ++++++++++++++++---------------- tmt/steps/provision/artemis.py | 2 +- tmt/steps/provision/connect.py | 2 +- tmt/steps/provision/local.py | 10 +++--- tmt/steps/provision/mrack.py | 2 +- tmt/steps/provision/podman.py | 2 +- tmt/steps/provision/testcloud.py | 2 +- 8 files changed, 40 insertions(+), 40 deletions(-) diff --git a/tmt/steps/finish/__init__.py b/tmt/steps/finish/__init__.py index ce68d0e3e6..11ae1b532f 100644 --- a/tmt/steps/finish/__init__.py +++ b/tmt/steps/finish/__init__.py @@ -226,7 +226,7 @@ def go(self, force: bool = False) -> None: # Stop and remove provisioned guests for guest in self.plan.provision.guests(): - guest.handle_guest_logs(log_names=guest.log_names) + guest.fetch_logs(lognames=guest.lognames) guest.stop() guest.remove() diff --git a/tmt/steps/provision/__init__.py b/tmt/steps/provision/__init__.py index a3d2091f16..862bfa8fcd 100644 --- a/tmt/steps/provision/__init__.py +++ b/tmt/steps/provision/__init__.py @@ -1114,7 +1114,7 @@ def scripts_path(self) -> Path: ) @property - def log_names(self) -> list[str]: + def lognames(self) -> list[str]: """ Return name list of logs the guest could provide. """ return [] @@ -1708,58 +1708,58 @@ def essential_requires(cls) -> list['tmt.base.Dependency']: return [] - def acquire_log(self, log_name: str) -> Optional[str]: + def acquire_log(self, logname: str) -> Optional[str]: """ Fetch and return content of a log. - :param log_name: name of the log. + :param logname: name of the log. :returns: content of the log, or ``None`` if the log cannot be retrieved. """ raise NotImplementedError def store_log( self, - log_path: Path, - log_content: str, - log_name: Optional[str] = None) -> None: + path: Path, + content: str, + logname: Optional[str] = None) -> None: """ Save log content to a file. - :param log_path: a path to save into, could be a directory + :param path: a path to save into, could be a directory or a file path. - :param log_content: content of the log. - :param log_name: name of the log, if not set, log_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 log_path is file path - if not log_path.is_dir(): - log_path.write_text(log_content) - # log_path is a directory - elif log_name: - (log_path / log_name).write_text(log_content) + # if path is file path + if not path.is_dir(): + path.write_text(content) + # if path is a directory + elif logname: + (path / logname).write_text(content) else: raise tmt.utils.GeneralError( 'Log path is a directory but log name is not defined.') - def handle_guest_logs(self, - log_path: Optional[Path] = None, - log_names: Optional[list[str]] = None) -> None: + def fetch_logs(self, + dirpath: Optional[Path] = None, + lognames: Optional[list[str]] = None) -> None: """ Get log content and save it to a directory. - :param log_path: a directory to save into. If not set, step's working 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 log_names: name list of logs need to be handled. If not set, all guest logs - would be collected, as reported by :py:attr:`log_names`. - """ - log_names = log_names or self.log_names - log_path = log_path or self.workdir or Path.cwd() - for log_name in log_names: - log_content = self.acquire_log(log_name) - if log_content: - self.store_log(log_path, log_content, log_name) + :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`. + """ + lognames = lognames or self.lognames + dirpath = dirpath or self.workdir or Path.cwd() + for logname in lognames: + content = self.acquire_log(logname) + if content: + self.store_log(dirpath, content, logname) else: - self.debug(f'no content in {log_name}') + self.store_log(dirpath, '', logname) @container diff --git a/tmt/steps/provision/artemis.py b/tmt/steps/provision/artemis.py index c6525f1821..3397a21a65 100644 --- a/tmt/steps/provision/artemis.py +++ b/tmt/steps/provision/artemis.py @@ -494,7 +494,7 @@ def is_ready(self) -> bool: return self.primary_address is not None @property - def log_names(self) -> list[str]: + def lognames(self) -> list[str]: """ Return name list of logs the guest could provide. """ return [] diff --git a/tmt/steps/provision/connect.py b/tmt/steps/provision/connect.py index cf20298d9d..4ca6157f0a 100644 --- a/tmt/steps/provision/connect.py +++ b/tmt/steps/provision/connect.py @@ -86,7 +86,7 @@ class GuestConnect(tmt.steps.provision.GuestSsh): hard_reboot: Optional[ShellScript] @property - def log_names(self) -> list[str]: + def lognames(self) -> list[str]: """ Return name list of logs the guest could provide. """ return [] diff --git a/tmt/steps/provision/local.py b/tmt/steps/provision/local.py index a060c54f04..f8d07f32a5 100644 --- a/tmt/steps/provision/local.py +++ b/tmt/steps/provision/local.py @@ -33,9 +33,9 @@ def is_ready(self) -> bool: return True @property - def log_names(self) -> list[str]: + def lognames(self) -> list[str]: """ Return name list of logs the guest could provide. """ - return ['dmesg'] + return [] def _run_ansible( self, @@ -184,14 +184,14 @@ def pull( Nothing to be done to pull workdir """ - def acquire_log(self, log_name: str) -> Optional[str]: + def acquire_log(self, logname: str) -> Optional[str]: """ Fetch and return content of a log. - :param log_name: name of the log. + :param logname: name of the log. :returns: content of the log, or ``None`` if the log cannot be retrieved. """ - if log_name == 'dmesg': + if logname == 'dmesg': return self.execute(Command('dmesg')).stdout return None diff --git a/tmt/steps/provision/mrack.py b/tmt/steps/provision/mrack.py index b81b64160b..fe8d3fb4fd 100644 --- a/tmt/steps/provision/mrack.py +++ b/tmt/steps/provision/mrack.py @@ -1187,7 +1187,7 @@ def is_ready(self) -> bool: return False @property - def log_names(self) -> list[str]: + def lognames(self) -> list[str]: """ Return name list of logs the guest could provide. """ return [] diff --git a/tmt/steps/provision/podman.py b/tmt/steps/provision/podman.py index 95c7450790..2e8f71992d 100644 --- a/tmt/steps/provision/podman.py +++ b/tmt/steps/provision/podman.py @@ -129,7 +129,7 @@ def is_ready(self) -> bool: return str(cmd_output.stdout).strip() == 'true' @property - def log_names(self) -> list[str]: + def lognames(self) -> list[str]: """ Return name list of logs the guest could provide. """ return [] diff --git a/tmt/steps/provision/testcloud.py b/tmt/steps/provision/testcloud.py index fd99ac242e..8d2c996c98 100644 --- a/tmt/steps/provision/testcloud.py +++ b/tmt/steps/provision/testcloud.py @@ -716,7 +716,7 @@ def is_coreos(self) -> bool: return bool(re.search('coreos|rhcos', self.image.lower())) @property - def log_names(self) -> list[str]: + def lognames(self) -> list[str]: """ Return name list of logs the guest could provide. """ return [] From 69452f92d54d92e31aa496d0444bf5f86222d145 Mon Sep 17 00:00:00 2001 From: lnie Date: Thu, 13 Feb 2025 22:10:16 -0500 Subject: [PATCH 7/9] squash:ruff-format --- tmt/steps/provision/__init__.py | 17 ++++++----------- tmt/steps/provision/artemis.py | 2 +- tmt/steps/provision/connect.py | 2 +- tmt/steps/provision/local.py | 2 +- tmt/steps/provision/mrack.py | 2 +- tmt/steps/provision/podman.py | 2 +- tmt/steps/provision/testcloud.py | 2 +- 7 files changed, 12 insertions(+), 17 deletions(-) diff --git a/tmt/steps/provision/__init__.py b/tmt/steps/provision/__init__.py index 862bfa8fcd..bd26ae2c2b 100644 --- a/tmt/steps/provision/__init__.py +++ b/tmt/steps/provision/__init__.py @@ -1115,7 +1115,7 @@ def scripts_path(self) -> Path: @property def lognames(self) -> list[str]: - """ Return name list of logs the guest could provide. """ + """Return name list of logs the guest could provide.""" return [] @@ -1717,11 +1717,7 @@ def acquire_log(self, logname: str) -> Optional[str]: """ raise NotImplementedError - def store_log( - self, - path: Path, - content: str, - logname: Optional[str] = None) -> None: + def store_log(self, path: Path, content: str, logname: Optional[str] = None) -> None: """ Save log content to a file. @@ -1738,12 +1734,11 @@ def store_log( elif logname: (path / logname).write_text(content) else: - raise tmt.utils.GeneralError( - 'Log path is a directory but log name is not defined.') + raise tmt.utils.GeneralError('Log path is a directory but log name is not defined.') - def fetch_logs(self, - dirpath: Optional[Path] = None, - lognames: Optional[list[str]] = None) -> None: + def fetch_logs( + self, dirpath: Optional[Path] = None, lognames: Optional[list[str]] = None + ) -> None: """ Get log content and save it to a directory. diff --git a/tmt/steps/provision/artemis.py b/tmt/steps/provision/artemis.py index 3397a21a65..bee705ae82 100644 --- a/tmt/steps/provision/artemis.py +++ b/tmt/steps/provision/artemis.py @@ -495,7 +495,7 @@ def is_ready(self) -> bool: @property def lognames(self) -> list[str]: - """ Return name list of logs the guest could provide. """ + """Return name list of logs the guest could provide.""" return [] def _create(self) -> None: diff --git a/tmt/steps/provision/connect.py b/tmt/steps/provision/connect.py index 4ca6157f0a..b94f85db38 100644 --- a/tmt/steps/provision/connect.py +++ b/tmt/steps/provision/connect.py @@ -87,7 +87,7 @@ class GuestConnect(tmt.steps.provision.GuestSsh): @property def lognames(self) -> list[str]: - """ Return name list of logs the guest could provide. """ + """Return name list of logs the guest could provide.""" return [] def reboot( diff --git a/tmt/steps/provision/local.py b/tmt/steps/provision/local.py index f8d07f32a5..4c58692d66 100644 --- a/tmt/steps/provision/local.py +++ b/tmt/steps/provision/local.py @@ -34,7 +34,7 @@ def is_ready(self) -> bool: @property def lognames(self) -> list[str]: - """ Return name list of logs the guest could provide. """ + """Return name list of logs the guest could provide.""" return [] def _run_ansible( diff --git a/tmt/steps/provision/mrack.py b/tmt/steps/provision/mrack.py index fe8d3fb4fd..f7dd8aeb6c 100644 --- a/tmt/steps/provision/mrack.py +++ b/tmt/steps/provision/mrack.py @@ -1188,7 +1188,7 @@ def is_ready(self) -> bool: @property def lognames(self) -> list[str]: - """ Return name list of logs the guest could provide. """ + """Return name list of logs the guest could provide.""" return [] def _create(self, tmt_name: str) -> None: diff --git a/tmt/steps/provision/podman.py b/tmt/steps/provision/podman.py index 2e8f71992d..4ffe9a71f7 100644 --- a/tmt/steps/provision/podman.py +++ b/tmt/steps/provision/podman.py @@ -130,7 +130,7 @@ def is_ready(self) -> bool: @property def lognames(self) -> list[str]: - """ Return name list of logs the guest could provide. """ + """Return name list of logs the guest could provide.""" return [] def wake(self) -> None: diff --git a/tmt/steps/provision/testcloud.py b/tmt/steps/provision/testcloud.py index 8d2c996c98..caa22e57e4 100644 --- a/tmt/steps/provision/testcloud.py +++ b/tmt/steps/provision/testcloud.py @@ -717,7 +717,7 @@ def is_coreos(self) -> bool: @property def lognames(self) -> list[str]: - """ Return name list of logs the guest could provide. """ + """Return name list of logs the guest could provide.""" return [] def _get_url(self, url: str, message: str) -> requests.Response: From 1db60372c8e691cc8ecab154da862fb559e69b78 Mon Sep 17 00:00:00 2001 From: lnie Date: Mon, 24 Feb 2025 04:59:33 -0500 Subject: [PATCH 8/9] squash:new implementation --- tmt/steps/finish/__init__.py | 2 +- tmt/steps/provision/__init__.py | 65 +++++++++++++++++++++++++------- tmt/steps/provision/artemis.py | 5 --- tmt/steps/provision/connect.py | 5 --- tmt/steps/provision/local.py | 16 -------- tmt/steps/provision/mrack.py | 29 +++++++++++--- tmt/steps/provision/podman.py | 5 --- tmt/steps/provision/testcloud.py | 5 --- 8 files changed, 77 insertions(+), 55 deletions(-) diff --git a/tmt/steps/finish/__init__.py b/tmt/steps/finish/__init__.py index 11ae1b532f..8b4d9bdaaa 100644 --- a/tmt/steps/finish/__init__.py +++ b/tmt/steps/finish/__init__.py @@ -226,7 +226,7 @@ def go(self, force: bool = False) -> None: # Stop and remove provisioned guests for guest in self.plan.provision.guests(): - guest.fetch_logs(lognames=guest.lognames) + guest.fetch_logs(guest_logs=guest.guest_logs) guest.stop() guest.remove() diff --git a/tmt/steps/provision/__init__.py b/tmt/steps/provision/__init__.py index bd26ae2c2b..d779785343 100644 --- a/tmt/steps/provision/__init__.py +++ b/tmt/steps/provision/__init__.py @@ -8,6 +8,7 @@ import re import secrets import shlex +import shutil import signal as _signal import string import subprocess @@ -56,6 +57,7 @@ ProvisionError, ShellScript, configure_constant, + create_directory, effective_workdir_root, ) @@ -975,6 +977,19 @@ def show( logger.info(key_to_option(key).replace('-', ' '), printable_value, color='green') +@container +class GuestLog: + name: str + + def fetch(self) -> Optional[str]: + """ + Fetch and return content of a log. + + :returns: content of the log, or ``None`` if the log cannot be retrieved. + """ + raise NotImplementedError + + class Guest(tmt.utils.Common): """ Guest provisioned for test execution @@ -1016,6 +1031,8 @@ def get_data_class(cls) -> type[GuestData]: #: Guest topology hostname or IP address for guest/guest communication. topology_address: Optional[str] = None + guest_logs: list[GuestLog] = [] + become: bool hardware: Optional[tmt.hardware.Hardware] @@ -1113,12 +1130,6 @@ def scripts_path(self) -> Path: else tmt.steps.execute.DEFAULT_SCRIPTS_DEST_DIR ) - @property - def lognames(self) -> list[str]: - """Return name list of logs the guest could provide.""" - - return [] - @classmethod def options(cls, how: Optional[str] = None) -> list[tmt.options.ClickOptionDecoratorType]: """ @@ -1737,7 +1748,7 @@ def store_log(self, path: Path, content: str, logname: Optional[str] = None) -> raise tmt.utils.GeneralError('Log path is a directory but log name is not defined.') def fetch_logs( - self, dirpath: Optional[Path] = None, lognames: Optional[list[str]] = None + self, dirpath: Optional[Path] = None, guest_logs: Optional[list[GuestLog]] = None ) -> None: """ Get log content and save it to a directory. @@ -1747,14 +1758,20 @@ def fetch_logs( :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`. """ - lognames = lognames or self.lognames - dirpath = dirpath or self.workdir or Path.cwd() - for logname in lognames: - content = self.acquire_log(logname) + + guest_logs = guest_logs or self.guest_logs or [] + + dirpath = dirpath or (self.workdir / 'logs' if self.workdir else None) or Path.cwd() + if self.workdir and dirpath == self.workdir / 'logs': + create_directory( + path=self.workdir / 'logs', name='logs workdir', quiet=True, logger=self._logger + ) + for log in guest_logs: + content = log.fetch() if content: - self.store_log(dirpath, content, logname) + self.store_log(dirpath, content, log.name) else: - self.store_log(dirpath, '', logname) + self.store_log(dirpath, '', log.name) @container @@ -2792,6 +2809,28 @@ def show(self, keys: Optional[list[str]] = None) -> None: if hardware: echo(tmt.utils.format('hardware', tmt.utils.dict_to_yaml(hardware.to_spec()))) + def prune(self, logger: tmt.log.Logger) -> None: + """Do not prune logs""" + if self.workdir is None: + return + + logs_dir = self.workdir / 'logs' + if logs_dir.exists(): + for member in self.workdir.iterdir(): + if member.name == "logs": + logger.debug(f"Preserve '{member.relative_to(self.workdir)}'.", level=3) + continue + 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}") + else: + super().prune(logger) + @container class ProvisionTask(tmt.queue.GuestlessTask[None]): diff --git a/tmt/steps/provision/artemis.py b/tmt/steps/provision/artemis.py index bee705ae82..53adb7ea2f 100644 --- a/tmt/steps/provision/artemis.py +++ b/tmt/steps/provision/artemis.py @@ -493,11 +493,6 @@ def is_ready(self) -> bool: # return True if self.guest is not None return self.primary_address is not None - @property - def lognames(self) -> list[str]: - """Return name list of logs the guest could provide.""" - return [] - def _create(self) -> None: environment: dict[str, Any] = { 'hw': {'arch': self.arch}, diff --git a/tmt/steps/provision/connect.py b/tmt/steps/provision/connect.py index b94f85db38..c9b330943a 100644 --- a/tmt/steps/provision/connect.py +++ b/tmt/steps/provision/connect.py @@ -85,11 +85,6 @@ class GuestConnect(tmt.steps.provision.GuestSsh): soft_reboot: Optional[ShellScript] hard_reboot: Optional[ShellScript] - @property - def lognames(self) -> list[str]: - """Return name list of logs the guest could provide.""" - return [] - def reboot( self, hard: bool = False, diff --git a/tmt/steps/provision/local.py b/tmt/steps/provision/local.py index 4c58692d66..4e043b1cd8 100644 --- a/tmt/steps/provision/local.py +++ b/tmt/steps/provision/local.py @@ -32,11 +32,6 @@ def is_ready(self) -> bool: return True - @property - def lognames(self) -> list[str]: - """Return name list of logs the guest could provide.""" - return [] - def _run_ansible( self, playbook: tmt.steps.provision.AnsibleApplicable, @@ -184,17 +179,6 @@ def pull( Nothing to be done to pull workdir """ - def acquire_log(self, logname: str) -> Optional[str]: - """ - Fetch and return content of a log. - - :param logname: name of the log. - :returns: content of the log, or ``None`` if the log cannot be retrieved. - """ - if logname == 'dmesg': - return self.execute(Command('dmesg')).stdout - return None - @tmt.steps.provides_method('local') class ProvisionLocal(tmt.steps.provision.ProvisionPlugin[ProvisionLocalData]): diff --git a/tmt/steps/provision/mrack.py b/tmt/steps/provision/mrack.py index f7dd8aeb6c..19cdb09f24 100644 --- a/tmt/steps/provision/mrack.py +++ b/tmt/steps/provision/mrack.py @@ -1186,11 +1186,6 @@ def is_ready(self) -> bool: except mrack.errors.MrackError: return False - @property - def lognames(self) -> list[str]: - """Return name list of logs the guest could provide.""" - return [] - def _create(self, tmt_name: str) -> None: """ Create beaker job xml request and submit it to Beaker hub @@ -1290,6 +1285,11 @@ 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(self, key.replace('.log', ''), response["logs"][key]) + ) + self.guest_logs.append(GuestLogBeaker(self, 'dmesg')) return current raise tmt.utils.WaitingIncompleteError @@ -1479,3 +1479,22 @@ def guest(self) -> Optional[GuestBeaker]: """ return self._guest + + +@container +class GuestLogBeaker(tmt.steps.provision.GuestLog): + def __init__(self, guest: GuestBeaker, name: str, url: Optional[str] = None) -> None: + self.name = name + self.url = url + self.guest = guest + + def fetch(self) -> Optional[str]: + """ + Fetch and return content of a log. + + :returns: content of the log, or ``None`` if the log cannot be retrieved. + """ + + if self.name == 'dmesg': + return self.guest.execute(Command('dmesg')).stdout + return tmt.utils.get_url_content(self.url) if self.url else None diff --git a/tmt/steps/provision/podman.py b/tmt/steps/provision/podman.py index 4ffe9a71f7..2869c346af 100644 --- a/tmt/steps/provision/podman.py +++ b/tmt/steps/provision/podman.py @@ -128,11 +128,6 @@ def is_ready(self) -> bool: ) return str(cmd_output.stdout).strip() == 'true' - @property - def lognames(self) -> list[str]: - """Return name list of logs the guest could provide.""" - return [] - def wake(self) -> None: """ Wake up the guest diff --git a/tmt/steps/provision/testcloud.py b/tmt/steps/provision/testcloud.py index caa22e57e4..b83da3a8f0 100644 --- a/tmt/steps/provision/testcloud.py +++ b/tmt/steps/provision/testcloud.py @@ -715,11 +715,6 @@ def is_coreos(self) -> bool: # Is this a CoreOS? return bool(re.search('coreos|rhcos', self.image.lower())) - @property - def lognames(self) -> list[str]: - """Return name list of logs the guest could provide.""" - return [] - def _get_url(self, url: str, message: str) -> requests.Response: """ Get url, retry when fails, return response From b4276e0f7a11b54cc9d13574b2388a8e7faff09c Mon Sep 17 00:00:00 2001 From: lnie Date: Tue, 25 Feb 2025 04:31:08 -0500 Subject: [PATCH 9/9] squash:update --- tmt/steps/provision/__init__.py | 91 +++++++++++---------------------- tmt/steps/provision/mrack.py | 19 ++++--- 2 files changed, 38 insertions(+), 72 deletions(-) diff --git a/tmt/steps/provision/__init__.py b/tmt/steps/provision/__init__.py index d779785343..7c89d5395c 100644 --- a/tmt/steps/provision/__init__.py +++ b/tmt/steps/provision/__init__.py @@ -8,7 +8,6 @@ import re import secrets import shlex -import shutil import signal as _signal import string import subprocess @@ -57,7 +56,6 @@ ProvisionError, ShellScript, configure_constant, - create_directory, effective_workdir_root, ) @@ -981,7 +979,7 @@ def show( class GuestLog: name: str - def fetch(self) -> Optional[str]: + def fetch(self) -> str: """ Fetch and return content of a log. @@ -989,6 +987,25 @@ def fetch(self) -> Optional[str]: """ 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): """ @@ -1031,8 +1048,6 @@ def get_data_class(cls) -> type[GuestData]: #: Guest topology hostname or IP address for guest/guest communication. topology_address: Optional[str] = None - guest_logs: list[GuestLog] = [] - become: bool hardware: Optional[tmt.hardware.Hardware] @@ -1058,6 +1073,7 @@ def __init__( """ Initialize guest data """ + self.guest_logs: list[GuestLog] = [] super().__init__(logger=logger, parent=parent, name=name) self.load(data) @@ -1719,33 +1735,12 @@ def essential_requires(cls) -> list['tmt.base.Dependency']: return [] - def acquire_log(self, logname: str) -> Optional[str]: - """ - Fetch and return content of a log. - - :param logname: name of the log. - :returns: content of the log, or ``None`` if the log cannot be retrieved. - """ - raise NotImplementedError - - def store_log(self, path: Path, content: str, logname: Optional[str] = None) -> None: + @property + def logdir(self) -> Optional[Path]: """ - 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. + Path to store logs """ - # if path is file path - if not path.is_dir(): - path.write_text(content) - # if path is a directory - elif logname: - (path / logname).write_text(content) - else: - raise tmt.utils.GeneralError('Log path is a directory but log name is not defined.') + return self.workdir / 'logs' if self.workdir else None def fetch_logs( self, dirpath: Optional[Path] = None, guest_logs: Optional[list[GuestLog]] = None @@ -1761,17 +1756,11 @@ def fetch_logs( guest_logs = guest_logs or self.guest_logs or [] - dirpath = dirpath or (self.workdir / 'logs' if self.workdir else None) or Path.cwd() - if self.workdir and dirpath == self.workdir / 'logs': - create_directory( - path=self.workdir / 'logs', name='logs workdir', quiet=True, logger=self._logger - ) + 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: - content = log.fetch() - if content: - self.store_log(dirpath, content, log.name) - else: - self.store_log(dirpath, '', log.name) + log.store(dirpath, log.name) @container @@ -2809,28 +2798,6 @@ def show(self, keys: Optional[list[str]] = None) -> None: if hardware: echo(tmt.utils.format('hardware', tmt.utils.dict_to_yaml(hardware.to_spec()))) - def prune(self, logger: tmt.log.Logger) -> None: - """Do not prune logs""" - if self.workdir is None: - return - - logs_dir = self.workdir / 'logs' - if logs_dir.exists(): - for member in self.workdir.iterdir(): - if member.name == "logs": - logger.debug(f"Preserve '{member.relative_to(self.workdir)}'.", level=3) - continue - 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}") - else: - super().prune(logger) - @container class ProvisionTask(tmt.queue.GuestlessTask[None]): diff --git a/tmt/steps/provision/mrack.py b/tmt/steps/provision/mrack.py index 19cdb09f24..3335c5029a 100644 --- a/tmt/steps/provision/mrack.py +++ b/tmt/steps/provision/mrack.py @@ -1287,9 +1287,12 @@ def get_new_state() -> GuestInspectType: if state == 'Reserved': for key in response["logs"]: self.guest_logs.append( - GuestLogBeaker(self, key.replace('.log', ''), response["logs"][key]) + GuestLogBeaker(key.replace('.log', ''), self, response["logs"][key]) ) - self.guest_logs.append(GuestLogBeaker(self, 'dmesg')) + # 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 @@ -1483,18 +1486,14 @@ def guest(self) -> Optional[GuestBeaker]: @container class GuestLogBeaker(tmt.steps.provision.GuestLog): - def __init__(self, guest: GuestBeaker, name: str, url: Optional[str] = None) -> None: - self.name = name - self.url = url - self.guest = guest + guest: GuestBeaker + url: str - def fetch(self) -> Optional[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. """ - if self.name == 'dmesg': - return self.guest.execute(Command('dmesg')).stdout - return tmt.utils.get_url_content(self.url) if self.url else None + return tmt.utils.get_url_content(self.url) if self.url else ''