From 5b8f42b160f8f8e3d6463a1110a3bc9e4e267dcc Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Wed, 31 Jan 2024 17:19:00 +0100 Subject: [PATCH] list/linfo with Rich + Remove terminaltables and Curses (#269) --- pyproject.toml | 1 - scripts/autocompletion/requirements.txt | 1 - setup.py | 1 - src/Kathara/cli/command/LinfoCommand.py | 50 ++++++++--------- src/Kathara/cli/command/ListCommand.py | 20 ++++--- src/Kathara/cli/ui/utils.py | 47 ++++++++-------- src/Kathara/foundation/manager/IManager.py | 13 +++-- src/Kathara/manager/Kathara.py | 13 +++-- src/Kathara/manager/docker/DockerLink.py | 40 +++++++------- src/Kathara/manager/docker/DockerMachine.py | 43 ++++++++------- src/Kathara/manager/docker/DockerManager.py | 31 ++++++----- .../manager/docker/stats/DockerLinkStats.py | 7 ++- .../docker/stats/DockerMachineStats.py | 6 +++ .../manager/kubernetes/KubernetesLink.py | 28 +++++----- .../manager/kubernetes/KubernetesMachine.py | 26 ++++----- .../manager/kubernetes/KubernetesManager.py | 29 ++++++---- src/Kathara/strings.py | 17 +++--- src/Kathara/trdparty/curses/__init__.py | 0 src/Kathara/trdparty/curses/curses.py | 54 ------------------- src/requirements.txt | 4 +- 20 files changed, 195 insertions(+), 236 deletions(-) delete mode 100644 src/Kathara/trdparty/curses/__init__.py delete mode 100644 src/Kathara/trdparty/curses/curses.py diff --git a/pyproject.toml b/pyproject.toml index 6cbcf025..b249c3e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,6 @@ dependencies = [ "docker>=7.0.0", "kubernetes>=23.3.0", "requests>=2.22.0", - "terminaltables>=3.1.0", "slug>=2.0", "deepdiff==6.2.2", "pyroute2", diff --git a/scripts/autocompletion/requirements.txt b/scripts/autocompletion/requirements.txt index 9006e3e6..63858553 100644 --- a/scripts/autocompletion/requirements.txt +++ b/scripts/autocompletion/requirements.txt @@ -1,6 +1,5 @@ binaryornot>=0.4.4; requests>=2.22.0; -terminaltables>=3.1.0; slug>=2.0; deepdiff>=4.0.9; kubernetes>=23.3.0; diff --git a/setup.py b/setup.py index 91df07a2..f2ccd0e0 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,6 @@ "docker>=7.0.0", "kubernetes>=23.3.0", "requests>=2.22.0", - "terminaltables>=3.1.0", "slug>=2.0", "deepdiff==6.2.2", "pyroute2", diff --git a/src/Kathara/cli/command/LinfoCommand.py b/src/Kathara/cli/command/LinfoCommand.py index 223106cc..270c4852 100644 --- a/src/Kathara/cli/command/LinfoCommand.py +++ b/src/Kathara/cli/command/LinfoCommand.py @@ -1,6 +1,8 @@ import argparse from typing import List +from rich.live import Live + from ..ui.utils import create_panel from ..ui.utils import create_table from ... import utils @@ -10,7 +12,6 @@ from ...model.Link import BRIDGE_LINK_NAME from ...parser.netkit.LabParser import LabParser from ...strings import strings, wiki_description -from ...trdparty.curses.curses import Curses class LinfoCommand(Command): @@ -84,47 +85,38 @@ def run(self, current_path: str, argv: List[str]) -> None: return if args['name']: - self.console.print( - create_panel( - str(next(Kathara.get_instance().get_machine_stats(args['name'], lab.hash))), - title=f"{args['name']} Information" - ) - ) + machine_stats = next(Kathara.get_instance().get_machine_stats(args['name'], lab.hash)) + message = str(machine_stats) if machine_stats else f"Device `{args['name']}` Not Found." + style = None if machine_stats else "red bold" + + self.console.print(create_panel(message, title=f"{args['name']} Information", style=style)) else: machines_stats = Kathara.get_instance().get_machines_stats(lab.hash) - print(next(create_table(machines_stats))) + self.console.print(create_table(machines_stats)) @staticmethod def _get_machine_live_info(lab: Lab, machine_name: str) -> None: - # TODO: Replace Curses with rich Live - Curses.get_instance().init_window() - - try: + with Live(None, refresh_per_second=1, screen=True) as live: while True: - Curses.get_instance().print_string( - create_panel("Device Information") + "\n" + - str(next(Kathara.get_instance().get_machine_stats(machine_name, lab.hash))) + "\n" - ) - finally: - Curses.get_instance().close() + machine_stats = next(Kathara.get_instance().get_machine_stats(machine_name, lab.hash)) + message = str(machine_stats) if machine_stats else f"Device `{machine_name}` Not Found." + style = None if machine_stats else "red bold" + + live.update(create_panel(message, title=f"{machine_name} Information", style=style)) @staticmethod def _get_lab_live_info(lab: Lab) -> None: machines_stats = Kathara.get_instance().get_machines_stats(lab.hash) - table = create_table(machines_stats) - - Curses.get_instance().init_window() - try: + with Live(None, refresh_per_second=1, screen=True) as live: while True: - Curses.get_instance().print_string(next(table)) - except StopIteration: - pass - finally: - Curses.get_instance().close() + table = create_table(machines_stats) + if not table: + break - @staticmethod - def _get_conf_info(lab: Lab, machine_name: str = None) -> None: + live.update(table) + + def _get_conf_info(self, lab: Lab, machine_name: str = None) -> None: if machine_name: self.console.print( create_panel( diff --git a/src/Kathara/cli/command/ListCommand.py b/src/Kathara/cli/command/ListCommand.py index 1c52a916..f8dee472 100644 --- a/src/Kathara/cli/command/ListCommand.py +++ b/src/Kathara/cli/command/ListCommand.py @@ -1,13 +1,14 @@ import argparse from typing import List, Optional +from rich.live import Live + from ..ui.utils import create_table from ... import utils from ...exceptions import PrivilegeError from ...foundation.cli.command.Command import Command from ...manager.Kathara import Kathara from ...strings import strings, wiki_description -from ...trdparty.curses.curses import Curses class ListCommand(Command): @@ -62,19 +63,16 @@ def run(self, current_path: str, argv: List[str]) -> None: self._get_live_info(machine_name=args['name'], all_users=all_users) else: machines_stats = Kathara.get_instance().get_machines_stats(machine_name=args['name'], all_users=all_users) - print(next(create_table(machines_stats))) + self.console.print(create_table(machines_stats)) @staticmethod def _get_live_info(machine_name: Optional[str], all_users: bool) -> None: machines_stats = Kathara.get_instance().get_machines_stats(machine_name=machine_name, all_users=all_users) - table = create_table(machines_stats) - - Curses.get_instance().init_window() - try: + with Live(None, refresh_per_second=1, screen=True) as live: while True: - Curses.get_instance().print_string(next(table)) - except StopIteration: - pass - finally: - Curses.get_instance().close() + table = create_table(machines_stats) + if not table: + break + + live.update(table) diff --git a/src/Kathara/cli/ui/utils.py b/src/Kathara/cli/ui/utils.py index ba20080d..ae60c9a3 100644 --- a/src/Kathara/cli/ui/utils.py +++ b/src/Kathara/cli/ui/utils.py @@ -4,14 +4,15 @@ import subprocess import sys from datetime import datetime -from typing import Any, Dict, Generator +from typing import Any, Dict, Generator, Optional from typing import Callable from rich import box +from rich.console import RenderableType, Group from rich.panel import Panel from rich.prompt import Confirm +from rich.table import Table from rich.text import Text -from terminaltables import DoubleTable from ... import utils from ...foundation.manager.stats.IMachineStats import IMachineStats @@ -37,35 +38,37 @@ def create_panel(message: str = "", **kwargs) -> Panel: justify=kwargs['justify'] if 'justify' in kwargs else None, ), title=kwargs['title'] if 'title' in kwargs else None, - title_align="center", box=box.SQUARE + title_align="center", + box=kwargs['box'] if 'box' in kwargs else box.SQUARE, ) -def create_table(streams: Generator[Dict[str, IMachineStats], None, None]) -> \ - Generator[str, None, None]: - table = DoubleTable([]) - table.inner_row_border = True +def create_table(streams: Generator[Dict[str, IMachineStats], None, None]) -> Optional[RenderableType]: + try: + result = next(streams) + except StopIteration: + return None - while True: - try: - result = next(streams) - except StopIteration: - return + ts_header = f"TIMESTAMP: {datetime.now()}" + if not result: + return Group( + Text(ts_header, style="italic", justify="center"), + create_panel("No Devices Found", style="red bold", justify="center", box=box.DOUBLE) + ) - if not result: - return + table = Table(title=ts_header, show_lines=True, expand=True, box=box.SQUARE_DOUBLE_HEAD) - table.table_data = [] - for item in result.values(): - row_data = item.to_dict() - row_data = dict(filter(lambda x: x[0] not in FORBIDDEN_TABLE_COLUMNS, row_data.items())) + for item in result.values(): + row_data = item.to_dict() + row_data = dict(filter(lambda x: x[0] not in FORBIDDEN_TABLE_COLUMNS, row_data.items())) - if not table.table_data: - table.table_data.append(list(map(lambda x: x.replace('_', ' ').upper(), row_data.keys()))) + if not table.columns: + for col in map(lambda x: x.replace('_', ' ').upper(), row_data.keys()): + table.add_column(col, header_style="blue") - table.table_data.append(row_data.values()) + table.add_row(*map(lambda x: str(x), row_data.values())) - yield "TIMESTAMP: %s" % datetime.now() + "\n\n" + table.table + return table def open_machine_terminal(machine) -> None: diff --git a/src/Kathara/foundation/manager/IManager.py b/src/Kathara/foundation/manager/IManager.py index 0d637b4f..eb12926c 100644 --- a/src/Kathara/foundation/manager/IManager.py +++ b/src/Kathara/foundation/manager/IManager.py @@ -364,7 +364,8 @@ def get_machines_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[ @abstractmethod def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, - lab: Optional[Lab] = None, all_users: bool = False) -> Generator[IMachineStats, None, None]: + lab: Optional[Lab] = None, all_users: bool = False) \ + -> Generator[Optional[IMachineStats], None, None]: """Return information of the specified device in a specified network scenario. Args: @@ -378,7 +379,8 @@ def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, l all_users (bool): If True, search the device among all the users devices. Returns: - IMachineStats: IMachineStats object containing the device info. + Generator[Optional[IMachineStats], None, None]: A generator containing the IMachineStats object + with the device info. Returns None if the device is not found. Raises: InvocationError: If a running network scenario hash or name is not specified. @@ -408,7 +410,8 @@ def get_links_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str @abstractmethod def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, - lab: Optional[Lab] = None, all_users: bool = False) -> Generator[ILinkStats, None, None]: + lab: Optional[Lab] = None, all_users: bool = False) \ + -> Generator[Optional[ILinkStats], None, None]: """Return information of the specified deployed network in a specified network scenario. Args: @@ -422,8 +425,8 @@ def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_nam all_users (bool): If True, return information about the networks of all users. Returns: - Generator[Dict[str, ILinkStats], None, None]: A generator containing dicts that has API Object - identifier as keys and ILinksStats objects as values. + Generator[Optional[ILinkStats], None, None]: A generator containing the ILinkStats object + with the network info. Returns None if the network is not found. Raises: InvocationError: If a running network scenario hash or name is not specified. diff --git a/src/Kathara/manager/Kathara.py b/src/Kathara/manager/Kathara.py index 1df6cb6b..53f5f4ab 100644 --- a/src/Kathara/manager/Kathara.py +++ b/src/Kathara/manager/Kathara.py @@ -380,7 +380,8 @@ def get_machines_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[ return self.manager.get_machines_stats(lab_hash, lab_name, lab, machine_name, all_users) def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, - lab: Optional[Lab] = None, all_users: bool = False) -> Generator[IMachineStats, None, None]: + lab: Optional[Lab] = None, all_users: bool = False) \ + -> Generator[Optional[IMachineStats], None, None]: """Return information of the specified device in a specified network scenario. Args: @@ -394,7 +395,8 @@ def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, l all_users (bool): If True, search the device among all the users devices. Returns: - IMachineStats: IMachineStats object containing the device info. + Generator[Optional[IMachineStats], None, None]: A generator containing the IMachineStats object + with the device info. Returns None if the device is not found. Raises: InvocationError: If a running network scenario hash or name is not specified. @@ -422,7 +424,8 @@ def get_links_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str return self.manager.get_links_stats(lab_hash, lab_name, lab, link_name, all_users) def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, - lab: Optional[Lab] = None, all_users: bool = False) -> Generator[ILinkStats, None, None]: + lab: Optional[Lab] = None, all_users: bool = False) \ + -> Generator[Optional[ILinkStats], None, None]: """Return information of the specified deployed network in a specified network scenario. Args: @@ -436,8 +439,8 @@ def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_nam all_users (bool): If True, return information about the networks of all users. Returns: - Generator[Dict[str, ILinkStats], None, None]: A generator containing dicts that has API Object - identifier as keys and ILinksStats objects as values. + Generator[Optional[ILinkStats], None, None]: A generator containing the ILinkStats object + with the network info. Returns None if the network is not found. Raises: InvocationError: If a running network scenario hash or name is not specified. diff --git a/src/Kathara/manager/docker/DockerLink.py b/src/Kathara/manager/docker/DockerLink.py index e2073738..a4d02575 100644 --- a/src/Kathara/manager/docker/DockerLink.py +++ b/src/Kathara/manager/docker/DockerLink.py @@ -13,7 +13,6 @@ from .stats.DockerLinkStats import DockerLinkStats from ... import utils from ...event.EventDispatcher import EventDispatcher -from ...exceptions import LinkNotFoundError from ...exceptions import PrivilegeError from ...model.ExternalLink import ExternalLink from ...model.Lab import Lab @@ -228,7 +227,7 @@ def get_links_api_objects_by_filters(self, lab_hash: str = None, link_name: str if link_name: filters["label"].append(f"name={link_name}") - return self.client.networks.list(filters=filters) + return self.client.networks.list(filters=filters, greedy=True) def get_links_stats(self, lab_hash: str = None, link_name: str = None, user: str = None) -> \ Generator[Dict[str, DockerLinkStats], None, None]: @@ -243,38 +242,35 @@ def get_links_stats(self, lab_hash: str = None, link_name: str = None, user: str Returns: Generator[Dict[str, DockerMachineStats], None, None]: A generator containing network names as keys and DockerLinkStats as values. - - Raises: - LinkNotFoundError: If the collision domains specified are not found. """ - networks = self.get_links_api_objects_by_filters(lab_hash=lab_hash, link_name=link_name, user=user) - if not networks: - if not link_name: - raise LinkNotFoundError("No collision domains found.") - else: - raise LinkNotFoundError(f"Collision domains with name {link_name} not found.") - - networks = sorted(networks, key=lambda x: x.name) - networks_stats = {} def load_link_stats(network): - networks_stats[network.name] = DockerLinkStats(network) + if network.name not in networks_stats: + networks_stats[network.name] = DockerLinkStats(network) - pool_size = utils.get_pool_size() - items = utils.chunk_list(networks, pool_size) + while True: + networks = self.get_links_api_objects_by_filters(lab_hash=lab_hash, link_name=link_name, user=user) + if not networks: + yield dict() - with Pool(pool_size) as links_pool: - for chunk in items: - links_pool.map(func=load_link_stats, iterable=chunk) + pool_size = utils.get_pool_size() + items = utils.chunk_list(networks, pool_size) + with Pool(pool_size) as links_pool: + for chunk in items: + links_pool.map(func=load_link_stats, iterable=chunk) - while True: - for network_stats in networks_stats.values(): + networks_to_remove = [] + for network_id, network_stats in networks_stats.items(): try: network_stats.update() except StopIteration: + networks_to_remove.append(network_id) continue + for k in networks_to_remove: + networks_stats.pop(k, None) + yield networks_stats def _delete_link(self, network: docker.models.networks.Network) -> None: diff --git a/src/Kathara/manager/docker/DockerMachine.py b/src/Kathara/manager/docker/DockerMachine.py index c45cd8a7..5b66bfac 100644 --- a/src/Kathara/manager/docker/DockerMachine.py +++ b/src/Kathara/manager/docker/DockerMachine.py @@ -16,7 +16,7 @@ from .stats.DockerMachineStats import DockerMachineStats from ... import utils from ...event.EventDispatcher import EventDispatcher -from ...exceptions import MountDeniedError, MachineAlreadyExistsError, MachineNotFoundError, DockerPluginError, \ +from ...exceptions import MountDeniedError, MachineAlreadyExistsError, DockerPluginError, \ MachineBinaryError, MachineNotRunningError from ...model.Interface import Interface from ...model.Lab import Lab @@ -845,7 +845,7 @@ def get_machines_api_objects_by_filters(self, lab_hash: str = None, machine_name if machine_name: filters["label"].append(f"name={machine_name}") - return self.client.containers.list(all=True, filters=filters) + return self.client.containers.list(all=True, filters=filters, ignore_removed=True) def get_machines_stats(self, lab_hash: str = None, machine_name: str = None, user: str = None) -> \ Generator[Dict[str, DockerMachineStats], None, None]: @@ -860,38 +860,37 @@ def get_machines_stats(self, lab_hash: str = None, machine_name: str = None, use Returns: Generator[Dict[str, DockerMachineStats], None, None]: A generator containing device names as keys and DockerMachineStats as values. - - Raises: - MachineNotFoundError: If the specified devices are not running. """ - containers = self.get_machines_api_objects_by_filters(lab_hash=lab_hash, machine_name=machine_name, user=user) - if not containers: - if not machine_name: - raise MachineNotFoundError("No devices found.") - else: - raise MachineNotFoundError(f"Devices with name {machine_name} not found.") - - containers = sorted(containers, key=lambda x: x.name) - machines_stats = {} def load_machine_stats(machine): - machines_stats[machine.name] = DockerMachineStats(machine) + if machine.name not in machines_stats: + machines_stats[machine.name] = DockerMachineStats(machine) - pool_size = utils.get_pool_size() - items = utils.chunk_list(containers, pool_size) + while True: + containers = self.get_machines_api_objects_by_filters( + lab_hash=lab_hash, machine_name=machine_name, user=user + ) + if not containers: + yield dict() - with Pool(pool_size) as machines_pool: - for chunk in items: - machines_pool.map(func=load_machine_stats, iterable=chunk) + pool_size = utils.get_pool_size() + items = utils.chunk_list(containers, pool_size) + with Pool(pool_size) as machines_pool: + for chunk in items: + machines_pool.map(func=load_machine_stats, iterable=chunk) - while True: - for machine_stats in machines_stats.values(): + machines_to_remove = [] + for machine_id, machine_stats in machines_stats.items(): try: machine_stats.update() except StopIteration: + machines_to_remove.append(machine_id) continue + for k in machines_to_remove: + machines_stats.pop(k, None) + yield machines_stats @staticmethod diff --git a/src/Kathara/manager/docker/DockerManager.py b/src/Kathara/manager/docker/DockerManager.py index f92341ba..4f22b85b 100644 --- a/src/Kathara/manager/docker/DockerManager.py +++ b/src/Kathara/manager/docker/DockerManager.py @@ -704,7 +704,7 @@ def get_machines_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[ @privileged def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, lab: Optional[Lab] = None, all_users: bool = False) \ - -> Generator[DockerMachineStats, None, None]: + -> Generator[Optional[DockerMachineStats], None, None]: """Return information of the specified device in a specified network scenario. Args: @@ -718,8 +718,8 @@ def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, l all_users (bool): If True, search the device among all the users devices. Returns: - Generator[DockerMachineStats, None, None]: A generator containing DockerMachineStats objects with - the device info. + Generator[Optional[DockerMachineStats], None, None]: A generator containing the DockerMachineStats object + with the device info. Returns None if the device is not found. Raises: InvocationError: If a running network scenario hash or name is not specified. @@ -731,9 +731,12 @@ def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, l lab_hash = utils.generate_urlsafe_hash(lab_name) machines_stats = self.get_machines_stats(lab_hash=lab_hash, machine_name=machine_name, all_users=all_users) - (_, machine_stats) = next(machines_stats).popitem() - - yield machine_stats + machines_stats_next = next(machines_stats) + if machines_stats_next: + (_, machine_stats) = machines_stats_next.popitem() + yield machine_stats + else: + yield None @privileged def get_links_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, lab: Optional[Lab] = None, @@ -766,7 +769,8 @@ def get_links_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str @privileged def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, - lab: Optional[Lab] = None, all_users: bool = False) -> Generator[DockerLinkStats, None, None]: + lab: Optional[Lab] = None, all_users: bool = False) \ + -> Generator[Optional[DockerLinkStats], None, None]: """Return information of the specified deployed network in a specified network scenario. Args: @@ -780,8 +784,8 @@ def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_nam all_users (bool): If True, return information about the networks of all users. Returns: - Generator[DockerLinkStats, None, None]: A generator containing DockerLinkStats objects with the network - statistics. + Generator[Optional[DockerLinkStats], None, None]: A generator containing the DockerLinkStats object + with the network info. Returns None if the network is not found. Raises: InvocationError: If a running network scenario hash or name is not specified. @@ -793,9 +797,12 @@ def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_nam lab_hash = utils.generate_urlsafe_hash(lab_name) links_stats = self.get_links_stats(lab_hash=lab_hash, link_name=link_name, all_users=all_users) - (_, link_stats) = next(links_stats).popitem() - - yield link_stats + links_stats_next = next(links_stats) + if links_stats_next: + (_, link_stats) = links_stats_next.popitem() + yield link_stats + else: + yield None @privileged def check_image(self, image_name: str) -> None: diff --git a/src/Kathara/manager/docker/stats/DockerLinkStats.py b/src/Kathara/manager/docker/stats/DockerLinkStats.py index 6658ff0b..04a248ea 100644 --- a/src/Kathara/manager/docker/stats/DockerLinkStats.py +++ b/src/Kathara/manager/docker/stats/DockerLinkStats.py @@ -1,5 +1,6 @@ from typing import Dict, Any, List +from docker.errors import NotFound from docker.models.containers import Container from docker.models.networks import Network @@ -39,7 +40,11 @@ def update(self) -> None: Returns: None """ - self.link_api_object.reload() + try: + self.link_api_object.reload() + except NotFound: + # Happens while deleting + pass self.containers = [container for container in self.link_api_object.containers] diff --git a/src/Kathara/manager/docker/stats/DockerMachineStats.py b/src/Kathara/manager/docker/stats/DockerMachineStats.py index 5c5c88f8..80ae3e1a 100644 --- a/src/Kathara/manager/docker/stats/DockerMachineStats.py +++ b/src/Kathara/manager/docker/stats/DockerMachineStats.py @@ -1,5 +1,6 @@ from typing import Dict, Any, Generator, Optional +from docker.errors import NotFound from docker.models.containers import Container from ....foundation.manager.stats.IMachineStats import IMachineStats @@ -53,6 +54,11 @@ def update(self) -> None: None """ updated_stats = next(self.stats) + try: + self.machine_api_object.reload() + except NotFound: + # Happens while deleting + pass self.status = self.machine_api_object.status self.pids = updated_stats['pids_stats']['current'] if 'current' in updated_stats['pids_stats'] else 0 diff --git a/src/Kathara/manager/kubernetes/KubernetesLink.py b/src/Kathara/manager/kubernetes/KubernetesLink.py index 1f6d8500..2388a2df 100644 --- a/src/Kathara/manager/kubernetes/KubernetesLink.py +++ b/src/Kathara/manager/kubernetes/KubernetesLink.py @@ -16,7 +16,6 @@ from .stats.KubernetesLinkStats import KubernetesLinkStats from ... import utils from ...event.EventDispatcher import EventDispatcher -from ...exceptions import LinkNotFoundError from ...model.Lab import Lab from ...model.Link import Link from ...setting.Setting import Setting @@ -226,36 +225,35 @@ def get_links_stats(self, lab_hash: str = None, link_name: str = None) -> \ Returns: Generator[Dict[str, KubernetesLinkStats], None, None]: A generator containing network names as keys and KubernetesLinkStats as values. - - Raises: - LinkNotFoundError: If the collision domains specified are not found. """ + networks_stats = {} + + def load_link_stats(network): + if network['metadata']['name'] not in networks_stats: + networks_stats[network['metadata']['name']] = KubernetesLinkStats(network) + while True: networks = self.get_links_api_objects_by_filters(lab_hash=lab_hash, link_name=link_name) if not networks: - if not link_name: - raise LinkNotFoundError("No collision domains found.") - else: - raise LinkNotFoundError(f"Collision domains with name {link_name} not found.") - - networks_stats = {} - - def load_link_stats(network): - networks_stats[network['metadata']['name']] = KubernetesLinkStats(network) + yield dict() pool_size = utils.get_pool_size() items = utils.chunk_list(networks, pool_size) - with Pool(pool_size) as links_pool: for chunk in items: links_pool.map(func=load_link_stats, iterable=chunk) - for network_stats in networks_stats.values(): + networks_to_remove = [] + for network_id, network_stats in networks_stats.items(): try: network_stats.update() except StopIteration: + networks_to_remove.append(network_id) continue + for k in networks_to_remove: + networks_stats.pop(k, None) + yield networks_stats def _build_definition(self, link: Link, network_id: int) -> Dict[str, str]: diff --git a/src/Kathara/manager/kubernetes/KubernetesMachine.py b/src/Kathara/manager/kubernetes/KubernetesMachine.py index b68919b8..83959a09 100644 --- a/src/Kathara/manager/kubernetes/KubernetesMachine.py +++ b/src/Kathara/manager/kubernetes/KubernetesMachine.py @@ -24,7 +24,7 @@ from .stats.KubernetesMachineStats import KubernetesMachineStats from ... import utils from ...event.EventDispatcher import EventDispatcher -from ...exceptions import MachineAlreadyExistsError, MachineNotFoundError, MachineNotReadyError, MachineNotRunningError +from ...exceptions import MachineAlreadyExistsError, MachineNotReadyError, MachineNotRunningError from ...model.Lab import Lab from ...model.Machine import Machine from ...setting.Setting import Setting @@ -834,32 +834,34 @@ def get_machines_stats(self, lab_hash: str = None, machine_name: str = None) -> Generator[Dict[str, KubernetesMachineStats], None, None]: A generator containing device name as keys and KubernetesMachineStats as values. """ + machines_stats = {} + + def load_machine_stats(pod): + if pod.metadata.name not in machines_stats: + machines_stats[pod.metadata.name] = KubernetesMachineStats(pod) + while True: pods = self.get_machines_api_objects_by_filters(lab_hash=lab_hash, machine_name=machine_name) if not pods: - if not machine_name: - raise MachineNotFoundError("No devices found.") - else: - raise MachineNotFoundError(f"Devices with name {machine_name} not found.") - - machines_stats = {} - - def load_machine_stats(pod): - machines_stats[pod.metadata.name] = KubernetesMachineStats(pod) + yield dict() pool_size = utils.get_pool_size() items = utils.chunk_list(pods, pool_size) - with Pool(pool_size) as machines_pool: for chunk in items: machines_pool.map(func=load_machine_stats, iterable=chunk) - for machine_stats in machines_stats.values(): + machines_to_remove = [] + for machine_id, machine_stats in machines_stats.items(): try: machine_stats.update() except StopIteration: + machines_to_remove.append(machine_id) continue + for k in machines_to_remove: + machines_stats.pop(k, None) + yield machines_stats @staticmethod diff --git a/src/Kathara/manager/kubernetes/KubernetesManager.py b/src/Kathara/manager/kubernetes/KubernetesManager.py index e46c42e1..d1a135dc 100644 --- a/src/Kathara/manager/kubernetes/KubernetesManager.py +++ b/src/Kathara/manager/kubernetes/KubernetesManager.py @@ -638,7 +638,7 @@ def get_machines_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[ def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, lab: Optional[Lab] = None, all_users: bool = False) \ - -> Generator[KubernetesMachineStats, None, None]: + -> Generator[Optional[KubernetesMachineStats], None, None]: """Return information of the specified device in a specified network scenario. Args: @@ -652,7 +652,8 @@ def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, l all_users (bool): If True, search the device among all the users devices. Returns: - KubernetesMachineStats: KubernetesMachineStats object containing the device info. + Generator[Optional[KubernetesMachineStats], None, None]: A generator containing the KubernetesMachineStats + object with the device info. Returns None if the device is not found. Raises: InvocationError: If a running network scenario hash or name is not specified. @@ -666,9 +667,12 @@ def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, l lab_hash = lab_hash.lower() machines_stats = self.get_machines_stats(lab_hash=lab_hash, machine_name=machine_name) - (_, machine_stats) = next(machines_stats).popitem() - - yield machine_stats + machines_stats_next = next(machines_stats) + if machines_stats_next: + (_, machine_stats) = machines_stats_next.popitem() + yield machine_stats + else: + yield None def get_links_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, lab: Optional[Lab] = None, link_name: str = None, all_users: bool = False) \ @@ -704,7 +708,7 @@ def get_links_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, lab: Optional[Lab] = None, all_users: bool = False) \ - -> Generator[KubernetesLinkStats, None, None]: + -> Generator[Optional[KubernetesLinkStats], None, None]: """Return information of the specified deployed network in a specified network scenario. Args: @@ -718,8 +722,8 @@ def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_nam all_users (bool): If True, return information about the networks of all users. Returns: - Generator[KubernetesLinkStats, None, None]: A generator containing KubernetesLinkStats objects with - the network statistics. + Generator[Optional[KubernetesLinkStats], None, None]: A generator containing the KubernetesLinkStats object + with the network info. Returns None if the network is not found. Raises: InvocationError: If a running network scenario hash or name is not specified. @@ -733,9 +737,12 @@ def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_nam lab_hash = lab_hash.lower() links_stats = self.get_links_stats(lab_hash=lab_hash, link_name=link_name, all_users=all_users) - (_, link_stats) = next(links_stats).popitem() - - yield link_stats + links_stats_next = next(links_stats) + if links_stats_next: + (_, link_stats) = links_stats_next.popitem() + yield link_stats + else: + yield None def check_image(self, image_name: str) -> None: """Useless. The Check of the image is delegated to Kubernetes. diff --git a/src/Kathara/strings.py b/src/Kathara/strings.py index 1b30c173..09aa6582 100644 --- a/src/Kathara/strings.py +++ b/src/Kathara/strings.py @@ -1,4 +1,5 @@ -from terminaltables import SingleTable +from rich.console import Console +from rich.table import Table strings = { "vstart": "Start a new Kathara device", @@ -22,13 +23,11 @@ def formatted_strings() -> str: - commands = [] + console = Console(record=True) + commands_table = Table(show_header=False, show_edge=False, show_lines=False, box=None) for item in strings.items(): - commands.append(list(item)) + commands_table.add_row(*item) - commands_table = SingleTable(commands) - commands_table.inner_heading_row_border = False - commands_table.outer_border = False - commands_table.inner_column_border = False - - return commands_table.table + with console.capture() as _: + console.print(commands_table) + return console.export_text() diff --git a/src/Kathara/trdparty/curses/__init__.py b/src/Kathara/trdparty/curses/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/Kathara/trdparty/curses/curses.py b/src/Kathara/trdparty/curses/curses.py deleted file mode 100644 index a525fa11..00000000 --- a/src/Kathara/trdparty/curses/curses.py +++ /dev/null @@ -1,54 +0,0 @@ -import curses - - -class Curses(object): - __slots__ = ['_window'] - - __instance = None - - @staticmethod - def get_instance(): - if Curses.__instance is None: - Curses() - - return Curses.__instance - - def __init__(self): - if Curses.__instance is not None: - raise Exception("This class is a singleton!") - else: - self._window = None - - Curses.__instance = self - - def init_window(self, noecho=True, nocbreak=True, timeout=1000, scrollok=True, keypad=True): - self._window = curses.initscr() - if noecho: - curses.noecho() - if nocbreak: - curses.nocbreak() - self._window.timeout(timeout) - self._window.scrollok(scrollok) - self._window.keypad(keypad) - - def print_string(self, string, erase=True): - if not self._window: - raise Exception("Window not initialized, call init_window first.") - - if erase: - self._window.erase() - - self._window.addstr(string) - self._window.refresh() - - key = self._window.getch() - if key == 3: # CTRL+C - raise KeyboardInterrupt - - def close(self): - curses.nocbreak() - self._window.keypad(False) - curses.echo() - curses.endwin() - - self._window = None diff --git a/src/requirements.txt b/src/requirements.txt index f0d8e502..a8971497 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -2,7 +2,6 @@ binaryornot>=0.4.4; docker>=7.0.0; kubernetes>=23.3.0; requests>=2.22.0; -terminaltables>=3.1.0; slug>=2.0; deepdiff==6.2.2; pyroute2; @@ -12,5 +11,4 @@ chardet; libtmux>=0.18.0; sys_platform == 'darwin' or sys_platform == 'linux' git+https://github.com/saghul/pyuv@master#egg=pyuv appscript>=1.1.0; sys_platform == 'darwin' -pypiwin32>=223; sys_platform == 'win32' -windows-curses>=2.1.0; sys_platform == 'win32' \ No newline at end of file +pypiwin32>=223; sys_platform == 'win32' \ No newline at end of file