From 89a7f2f998c99b974d6ae0c6d1847ca0ae50a376 Mon Sep 17 00:00:00 2001 From: Israel Fruchter Date: Tue, 28 May 2024 23:31:46 +0300 Subject: [PATCH] feature(monitoring): take screenshots with grafana image renderer - switch to grafana image renderer - drop all of webdriver related implemetion - remove grafana snapshots - it's not in use for quite some time --- sdcm/cluster.py | 23 +---- sdcm/logcollector.py | 103 ++------------------ sdcm/monitorstack/ui.py | 167 +-------------------------------- sdcm/utils/remotewebbrowser.py | 132 -------------------------- 4 files changed, 17 insertions(+), 408 deletions(-) delete mode 100644 sdcm/utils/remotewebbrowser.py diff --git a/sdcm/cluster.py b/sdcm/cluster.py index 43a91a0c566..a6a1f0b5092 100644 --- a/sdcm/cluster.py +++ b/sdcm/cluster.py @@ -104,7 +104,6 @@ check_schema_version, check_nulls_in_peers, check_schema_agreement_in_gossip_and_peers, \ check_group0_tokenring_consistency, CHECK_NODE_HEALTH_RETRIES, CHECK_NODE_HEALTH_RETRY_DELAY from sdcm.utils.decorators import NoValue, retrying, log_run_info, optional_cached_property -from sdcm.utils.remotewebbrowser import WebDriverContainerMixin from sdcm.test_config import TestConfig from sdcm.utils.sstable.sstable_utils import SstableUtils from sdcm.utils.version_utils import ( @@ -127,7 +126,7 @@ from sdcm.sct_events.filters import EventsSeverityChangerFilter from sdcm.utils.auto_ssh import AutoSshContainerMixin from sdcm.monitorstack.ui import AlternatorDashboard -from sdcm.logcollector import GrafanaSnapshot, GrafanaScreenShot, PrometheusSnapshots, upload_archive_to_s3, \ +from sdcm.logcollector import GrafanaScreenShot, PrometheusSnapshots, upload_archive_to_s3, \ save_kallsyms_map, collect_diagnostic_data from sdcm.utils.ldap import LDAP_SSH_TUNNEL_LOCAL_PORT, LDAP_BASE_OBJECT, LDAP_PASSWORD, LDAP_USERS, \ LDAP_PORT, DEFAULT_PWD_SUFFIX @@ -229,7 +228,7 @@ def destroy(self): pass -class BaseNode(AutoSshContainerMixin, WebDriverContainerMixin): # pylint: disable=too-many-instance-attributes,too-many-public-methods +class BaseNode(AutoSshContainerMixin): # pylint: disable=too-many-instance-attributes,too-many-public-methods CQL_PORT = 9042 CQL_SSL_PORT = 9142 MANAGER_AGENT_PORT = 10001 @@ -5686,12 +5685,10 @@ def get_grafana_screenshot_and_snapshot(self, test_start_time: Optional[int] = N return {} screenshot_links = [] - snapshot_links = [] for node in self.nodes: screenshot_links.extend(self.get_grafana_screenshots(node, test_start_time)) - snapshot_links.extend(self.get_grafana_snapshots(node, test_start_time)) - return {'screenshots': screenshot_links, 'snapshots': snapshot_links} + return {'screenshots': screenshot_links} def get_grafana_screenshots(self, node: BaseNode, test_start_time: float) -> list[str]: screenshot_links = [] @@ -5710,20 +5707,6 @@ def get_grafana_screenshots(self, node: BaseNode, test_start_time: float) -> lis return screenshot_links - def get_grafana_snapshots(self, node: BaseNode, test_start_time: float) -> list[str]: - snapshot_links = [] - grafana_extra_dashboards = [] - if 'alternator_port' in self.params: - grafana_extra_dashboards = [AlternatorDashboard()] - - snapshots_collector = GrafanaSnapshot(name="grafana-snapshot", - test_start_time=test_start_time, - extra_entities=grafana_extra_dashboards) - snapshots_data = snapshots_collector.collect(node, self.logdir) - snapshot_links.extend(snapshots_data.get('links', [])) - - return snapshot_links - def upload_annotations_to_s3(self): annotations_url = '' if not self.nodes: diff --git a/sdcm/logcollector.py b/sdcm/logcollector.py index 563f0ebf908..0184c482997 100644 --- a/sdcm/logcollector.py +++ b/sdcm/logcollector.py @@ -54,20 +54,18 @@ get_sct_root_path, normalize_ipv6_url, create_remote_storage_dir, ) -from sdcm.utils.auto_ssh import AutoSshContainerMixin from sdcm.utils.context_managers import environment from sdcm.utils.decorators import retrying from sdcm.utils.docker_utils import get_docker_bridge_gateway from sdcm.utils.get_username import get_username from sdcm.utils.k8s import KubernetesOps -from sdcm.utils.remotewebbrowser import RemoteBrowser, WebDriverContainerMixin from sdcm.utils.s3_remote_uploader import upload_remote_files_directly_to_s3 from sdcm.utils.gce_utils import gce_public_addresses, gce_private_addresses LOGGER = logging.getLogger(__name__) -class CollectingNode(AutoSshContainerMixin, WebDriverContainerMixin): +class CollectingNode: # pylint: disable=too-few-public-methods,too-many-instance-attributes logdir = None @@ -440,7 +438,7 @@ class GrafanaEntity(BaseMonitoringEntity): # pylint: disable=too-few-public-met ] grafana_port = 3000 - grafana_entity_url_tmpl = "http://{node_ip}:{grafana_port}{path}?from={st}&to=now&refresh=1d" + grafana_entity_url_tmpl = "http://{node_ip}:{grafana_port}/render{path}?from={st}&to=now&refresh=1d" sct_base_path = get_sct_root_path() def __init__(self, *args, **kwargs): @@ -450,18 +448,8 @@ def __init__(self, *args, **kwargs): test_start_time = time.time() - (6 * 3600) self.start_time = str(test_start_time).split('.', maxsplit=1)[0] + '000' self.grafana_dashboards = self.base_grafana_dashboards + kwargs.pop("extra_entities", []) - self.remote_browser = None super().__init__(*args, **kwargs) - def close_browser(self): - if self.remote_browser: - LOGGER.info('Grafana - browser quit') - self.remote_browser.quit() - - def destory_webdriver_container(self): - if self.remote_browser: - self.remote_browser.destroy_containers() - def get_version(self, node): _, _, version = self.get_monitoring_version(node) if version: @@ -480,7 +468,6 @@ class GrafanaScreenShot(GrafanaEntity): GrafanaEntity """ - @retrying(n=5) def get_grafana_screenshot(self, node, local_dst): """ Take screenshot of the Grafana per-server-metrics dashboard and upload to S3 @@ -491,7 +478,6 @@ def get_grafana_screenshot(self, node, local_dst): return screenshots try: - self.remote_browser = RemoteBrowser(node) for dashboard in self.grafana_dashboards: try: dashboard_metadata = MonitoringStack.get_dashboard_by_title( @@ -512,11 +498,14 @@ def get_grafana_screenshot(self, node, local_dst): dashboard.name, datetime.datetime.now().strftime("%Y%m%d_%H%M%S"), node.name)) - self.remote_browser.open(grafana_url, dashboard.resolution) - dashboard.scroll_to_bottom(self.remote_browser.browser) - dashboard.wait_panels_loading(self.remote_browser.browser) LOGGER.debug("Get screenshot for url %s, save to %s", grafana_url, screenshot_path) - self.remote_browser.get_screenshot(grafana_url, screenshot_path) + with requests.get(grafana_url, stream=True, + params=dict(width=dashboard.resolution[0], + height=dashboard.resolution[1])) as response: + response.raise_for_status() + with open(screenshot_path, 'wb') as output_file: + for chunk in response.iter_content(chunk_size=8192): + output_file.write(chunk) screenshots.append(screenshot_path) except Exception as details: # pylint: disable=broad-except LOGGER.error("Error get screenshot %s: %s", dashboard.name, details, exc_info=True) @@ -526,8 +515,6 @@ def get_grafana_screenshot(self, node, local_dst): except Exception as details: # pylint: disable=broad-except LOGGER.error("Error taking monitor screenshot: %s, traceback: %s", details, traceback.format_exc()) return [] - finally: - self.close_browser() def collect(self, node, local_dst, remote_dst=None, local_search_path=None): node.logdir = local_dst @@ -535,77 +522,6 @@ def collect(self, node, local_dst, remote_dst=None, local_search_path=None): return self.get_grafana_screenshot(node, local_dst) -class GrafanaSnapshot(GrafanaEntity): - """Grafana snapshot - - Collect Grafana snapshot - - Extends: - GrafanaEntity - """ - @retrying(n=5) - def get_grafana_snapshot(self, node): - """ - Take snapshot of the Grafana per-server-metrics dashboard and upload to S3 - """ - snapshots = [] - version = self.get_version(node) - if not version: - return snapshots - try: - self.remote_browser = RemoteBrowser(node) - monitoring_ui.Login(self.remote_browser.browser, - ip=normalize_ipv6_url(node.grafana_address), - port=self.grafana_port).use_default_creds() - for dashboard in self.grafana_dashboards: - try: - dashboard_metadata = MonitoringStack.get_dashboard_by_title( - grafana_ip=normalize_ipv6_url(node.grafana_address), - port=self.grafana_port, - title=dashboard.title) - if not dashboard_metadata: - LOGGER.error("Dashboard '%s' was not found", dashboard.title) - continue - - grafana_url = self.grafana_entity_url_tmpl.format( - node_ip=normalize_ipv6_url(node.grafana_address), - grafana_port=self.grafana_port, - path=dashboard_metadata["url"], - st=self.start_time) - LOGGER.info("Get snapshot link for url %s", grafana_url) - self.remote_browser.open(grafana_url, dashboard.resolution) - dashboard.scroll_to_bottom(self.remote_browser.browser) - dashboard.wait_panels_loading(self.remote_browser.browser) - - snapshots.append(dashboard.get_snapshot(self.remote_browser.browser)) - except Exception as details: # pylint: disable=broad-except - LOGGER.error("Error get snapshot %s: %s, traceback: %s", - dashboard.name, details, traceback.format_exc()) - - LOGGER.info(snapshots) - return snapshots - - except Exception as details: # pylint: disable=broad-except - LOGGER.error("Error taking monitor snapshot: %s, traceback: %s", details, traceback.format_exc()) - return [] - finally: - self.close_browser() - - def collect(self, node, local_dst, remote_dst=None, local_search_path=None): - node.logdir = local_dst - os.makedirs(local_dst, exist_ok=True) - snapshots = self.get_grafana_snapshot(node) - snapshots_file = os.path.join(local_dst, "grafana_snapshots") - with open(snapshots_file, "w", encoding="utf-8") as f: # pylint: disable=invalid-name - for snapshot in snapshots: - f.write(snapshot + '\n') - - return {'links': snapshots, 'file': snapshots_file} - - def __del__(self): - self.destory_webdriver_container() - - class LogCollector: """Base class for LogCollector types @@ -950,7 +866,6 @@ class MonitorLogCollector(LogCollector): PrometheusSnapshots(name='prometheus_data'), MonitoringStack(name='monitoring-stack'), GrafanaScreenShot(name='grafana-screenshot'), - GrafanaSnapshot(name='grafana-snapshot') ] cluster_log_type = "monitor-set" cluster_dir_prefix = "monitor-set" diff --git a/sdcm/monitorstack/ui.py b/sdcm/monitorstack/ui.py index ab46a2de360..f8b0e06deeb 100644 --- a/sdcm/monitorstack/ui.py +++ b/sdcm/monitorstack/ui.py @@ -1,162 +1,20 @@ -import logging - -from typing import Tuple, List - -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.common.by import By -from selenium.webdriver.remote.webelement import WebElement -from selenium.common import exceptions - from sdcm.utils.ci_tools import get_test_name - -LOGGER = logging.getLogger(__name__) -UI_ELEMENT_LOAD_TIMEOUT = 180 -GRAFANA_USERNAME = "admin" -GRAFANA_PASSWORD = "admin" - - -class Login: - path = "http://{ip}:{port}/login" - username_locator = (By.XPATH, "//input[@name='user']") - password_locator = (By.XPATH, "//input[@name='password']") - login_button = (By.XPATH, "//button/span[contains(text(), 'Log in')]") - skip_button = (By.XPATH, "//button/span[contains(text(), 'Skip')]") - - def __init__(self, remote_browser, ip, port): - self.browser = remote_browser - LOGGER.info("open url: %s", self.path.format(ip=ip, port=port)) - self.browser.get(self.path.format(ip=ip, port=port)) - - def use_default_creds(self): - LOGGER.info("Login to grafana with default credentials") - try: - WebDriverWait(self.browser, UI_ELEMENT_LOAD_TIMEOUT).until( - EC.visibility_of_element_located(self.username_locator)) - username_element: WebElement = self.browser.find_element(*self.username_locator) - password_element: WebElement = self.browser.find_element(*self.password_locator) - username_element.clear() - username_element.send_keys(GRAFANA_USERNAME) - password_element.clear() - password_element.send_keys(GRAFANA_PASSWORD) - login_button: WebElement = self.browser.find_element(*self.login_button) - login_button.click() - self.skip_set_new_password() - LOGGER.info("Logged in succesful") - except Exception as details: # pylint: disable=broad-except - LOGGER.error("Authentication failed: %s", details) - - def skip_set_new_password(self): - WebDriverWait(self.browser, UI_ELEMENT_LOAD_TIMEOUT).until( - EC.visibility_of_element_located(self.skip_button)) - skip_element: WebElement = self.browser.find_element(*self.skip_button) - skip_element.click() - - -class Panel: - xpath_tmpl = """//h6[contains(@title,'{name}') and contains(@class, 'panel-title')]""" - - def __init__(self, name): - self.name = name - - @property - def xpath_locator(self): - return (By.XPATH, self.xpath_tmpl.format(name=self.name)) - - def wait_loading(self, remote_browser): - LOGGER.info("Waiting panel %s loading", self.name) - WebDriverWait(remote_browser, UI_ELEMENT_LOAD_TIMEOUT).until( - EC.visibility_of_element_located(self.xpath_locator)) - panel_title: WebElement = remote_browser.find_element(*self.xpath_locator) - panel_elem: WebElement = panel_title.find_element_by_xpath("parent::*//parent::*") - try: - loading = panel_elem.find_element_by_xpath("div[contains(@class, 'panel-loading')]") - WebDriverWait(remote_browser, UI_ELEMENT_LOAD_TIMEOUT).until(EC.invisibility_of_element(loading)) - LOGGER.debug("Panel %s could be without data", self.name) - except exceptions.NoSuchElementException: - LOGGER.debug("Panel %s loaded", self.name) - except exceptions.TimeoutException: - LOGGER.warning("Panel %s is still loading. Data on panel could displayed with delay", self.name) - LOGGER.debug("Panel %s was not fully loaded", self.name) - LOGGER.info("Work with panel %s done", self.name) - - -class Snapshot: # pylint: disable=too-few-public-methods - locators_sequence = [ - (By.XPATH, """//button[contains(@aria-label, "Share dashboard or panel")]"""), - (By.XPATH, """//div/a[contains(@aria-label, "Tab Snapshot") and contains(text(), "Snapshot")]"""), - (By.XPATH, """//button//span[contains(text(), "Publish to snapshots.raintank.io")]"""), - (By.XPATH, """//input[contains(@value, "https://snapshots.raintank.io")]""") - ] - - def get_shared_link(self, remote_browser): - """Get link from page to remote snapshot on https://snapshot.raintank.io - - using selenium remote web driver remote_browser find sequentially web_element by locators - in self.snapshot_locators_sequence run actiom WebElement.click() and - get value from result link found by latest element in snapshot_locators_sequence - - :param remote_browser: remote webdirver instance - :type remote_browser: selenium.webdriver.Remote - :returns: return value of link to remote snapshot on https://snapshot.raintank.io - :rtype: {str} - """ - for element in self.locators_sequence[:-1]: - LOGGER.debug("Search element '%s'", element) - WebDriverWait(remote_browser, UI_ELEMENT_LOAD_TIMEOUT).until(EC.visibility_of_element_located(element)) - found_element = remote_browser.find_element(*element) - found_element.click() - snapshot_link_locator = self.locators_sequence[-1] - WebDriverWait(remote_browser, UI_ELEMENT_LOAD_TIMEOUT).until( - EC.visibility_of_element_located(snapshot_link_locator)) - snapshot_link_element = remote_browser.find_element(*snapshot_link_locator) - - LOGGER.debug(snapshot_link_element.get_attribute("value")) - return snapshot_link_element.get_attribute("value") +# pylint: disable=too-few-public-methods class Dashboard: name: str path: str - resolution: str - scroll_ready_locator: Tuple[By, str] = (By.XPATH, "//div[@class='scrollbar-view']") - panels: List[Panel] - scroll_step: int = 1000 + resolution: tuple[int] title: str - def scroll_to_bottom(self, remote_browser): - WebDriverWait(remote_browser, UI_ELEMENT_LOAD_TIMEOUT).until( - EC.visibility_of_element_located(self.scroll_ready_locator)) - scroll_element = remote_browser.find_element(*self.scroll_ready_locator) - scroll_height = remote_browser.execute_script("return arguments[0].scrollHeight", scroll_element) - - for scroll_size in range(0, scroll_height, self.scroll_step): - LOGGER.debug("Scrolling next %s", self.scroll_step) - remote_browser.execute_script(f'arguments[0].scrollTop = {scroll_size}', scroll_element) - - def wait_panels_loading(self, remote_browser): - LOGGER.info("Waiting panels load data") - for panel in self.panels: - panel.wait_loading(remote_browser) - LOGGER.info("All panels have loaded data") - - @staticmethod - def get_snapshot(remote_browser): - return Snapshot().get_shared_link(remote_browser) - class OverviewDashboard(Dashboard): name = 'overview' path = 'd/overview-{version}/scylla-{dashboard_name}' title = 'Overview' - resolution = '1920px*4000px' - panels = [Panel("Average Disk Usage"), - Panel("Running Compactions"), - Panel("Writes"), - Panel("Write Latencies"), - Panel("Read Timeouts by DC"), - Panel("Write Timeouts by DC")] + resolution = (1920, 4000) class ServerMetricsNemesisDashboard(Dashboard): @@ -166,26 +24,11 @@ class ServerMetricsNemesisDashboard(Dashboard): name = f'{test_name}scylla-per-server-metrics-nemesis' title = 'Scylla Per Server Metrics Nemesis' path = 'dashboard/db/{dashboard_name}-{version}' - resolution = '1920px*15000px' - panels = [Panel("Total Requests"), - Panel("Load per Instance"), - Panel("Requests Served per Instance"), - Panel("Reads with no misses"), - Panel("Total Bytes"), - Panel("Running Compactions"), - Panel("Gemini metrics") - ] + resolution = (1920, 15000) class AlternatorDashboard(Dashboard): name = 'alternator' title = 'Alternator' path = 'd/alternator-{version}/{dashboard_name}' - resolution = '1920px*4000px' - panels = [Panel("Total Actions"), - Panel("Scan by Instance"), - Panel("Completed GetItem"), - Panel("95th percentile DeleteItem latency by Instance"), - Panel("Cache Hits"), - Panel("Your Graph here") - ] + resolution = (1920, 4000) diff --git a/sdcm/utils/remotewebbrowser.py b/sdcm/utils/remotewebbrowser.py deleted file mode 100644 index db2849f0277..00000000000 --- a/sdcm/utils/remotewebbrowser.py +++ /dev/null @@ -1,132 +0,0 @@ -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# -# See LICENSE for more details. -# -# Copyright (c) 2020 ScyllaDB - -import time -import logging -import os -from typing import Optional -from functools import cached_property - -import paramiko -from docker import DockerClient # pylint: disable=wrong-import-order -from selenium.webdriver import Remote, ChromeOptions - -from sdcm.utils.docker_utils import ContainerManager, DOCKER_API_CALL_TIMEOUT -from sdcm.utils.common import get_free_port, wait_for_port, normalize_ipv6_url -from sdcm.utils.ssh_agent import SSHAgent - - -WEB_DRIVER_IMAGE = "selenium/standalone-chrome:3.141.59-20210713" -WEB_DRIVER_REMOTE_PORT = 4444 -WEB_DRIVER_CONTAINER_START_DELAY = 30 # seconds - -LOGGER = logging.getLogger(__name__) - - -class WebDriverContainerMixin: - def web_driver_container_run_args(self) -> dict: - return dict(image=WEB_DRIVER_IMAGE, - name=f"{self.name}-webdriver", - ports={f"{WEB_DRIVER_REMOTE_PORT}/tcp": None, }, - privileged=True, - volumes={"/dev/shm": {"bind": "/dev/shm"}, }) - - @property - def web_driver_docker_client(self) -> Optional[DockerClient]: - if not self.ssh_login_info or self.ssh_login_info["key_file"] is None: - # running with docker backend, no real monitor node, fallback to use local docker - return None - SSHAgent.add_keys((self.ssh_login_info["key_file"], )) - # since a bug in docker package https://github.com/docker-library/python/issues/517 that need to explicitly - # pass down the port for supporting ipv6 - user = self.ssh_login_info['user'] - hostname = normalize_ipv6_url(self.ssh_login_info['hostname']) - try: - return DockerClient(base_url=f"ssh://{user}@{hostname}:22", timeout=DOCKER_API_CALL_TIMEOUT) - except paramiko.ssh_exception.BadHostKeyException as exc: - system_host_keys_path = os.path.expanduser("~/.ssh/known_hosts") - system_host_keys = paramiko.hostkeys.HostKeys(system_host_keys_path) - if system_host_keys.pop(exc.hostname, None): - system_host_keys.save(system_host_keys_path) - return DockerClient(base_url=f"ssh://{user}@{hostname}:22", timeout=DOCKER_API_CALL_TIMEOUT) - - -class RemoteBrowser: - def __init__(self, node, use_tunnel=True): - self.node = node - backend = self.node.parent_cluster.params.get( - "cluster_backend") if hasattr(self.node, 'parent_cluster') else None - self.use_tunnel = bool(self.node.ssh_login_info and use_tunnel and backend not in ('docker',)) - - @cached_property - def browser(self): - ContainerManager.run_container(self.node, "web_driver") - - LOGGER.debug("Waiting for WebDriver container is up") - ContainerManager.wait_for_status(self.node, "web_driver", "running") - - port = ContainerManager.get_container_port(self.node, "web_driver", WEB_DRIVER_REMOTE_PORT) - LOGGER.debug("WebDriver port is %s", port) - - if self.use_tunnel: - LOGGER.debug("Start auto_ssh for Selenium remote WebDriver") - ContainerManager.run_container(self.node, "auto_ssh:web_driver", - local_port=port, - remote_port=get_free_port(), - ssh_mode="-L") - - LOGGER.debug("Waiting for SSH tunnel container is up") - ContainerManager.wait_for_status(self.node, "auto_ssh:web_driver", status="running") - - host = "127.0.0.1" - port = int(ContainerManager.get_environ(self.node, "auto_ssh:web_driver")["SSH_TUNNEL_REMOTE"]) - else: - host = self.node.external_address - - LOGGER.debug("Waiting for port %s:%s is accepting connections", host, port) - wait_for_port(host, port) - - time.sleep(WEB_DRIVER_CONTAINER_START_DELAY) - - return Remote(command_executor=f"http://{host}:{port}/wd/hub", options=ChromeOptions()) - - def open(self, url, resolution="1920px*1280px"): - LOGGER.info("Set resolution %s", resolution) - self.browser.set_window_size(*resolution.replace("px", "").split("*", 1)) - LOGGER.info("Get url %s", url) - self.browser.get(url) - - def get_screenshot(self, url, screenshot_path): - LOGGER.info("Save a screenshot of %s to %s", url, screenshot_path) - - self.browser.get_screenshot_as_file(screenshot_path) - - def quit(self): - self.browser.quit() - - def destroy_containers(self): - names = ["web_driver"] - if self.use_tunnel: - names.append("auto_ssh:web_driver") - - for container_name in names: - self.destroy_container(container_name) - - def destroy_container(self, name): - try: - if ContainerManager.get_container(self.node, name, raise_not_found_exc=False) is not None: - LOGGER.debug("Destroy %s (%s) container", name, self) - ContainerManager.destroy_container(self.node, name, ignore_keepalive=True) - LOGGER.info("%s (%s) destroyed", name, self) - except Exception as exc: # pylint: disable=broad-except - LOGGER.error("%s: some exception raised during container '%s' destroying", self, name, exc_info=exc)