edge-containers-cli 3.5.0__tar.gz → 3.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/PKG-INFO +1 -1
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/pyproject.toml +4 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/_version.py +2 -2
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/cmds/cli.py +6 -6
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/cmds/commands.py +4 -4
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/cmds/k8s_commands.py +21 -10
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/cmds/local_commands.py +6 -6
- edge_containers_cli-3.5.2/src/edge_containers_cli/cmds/monitor.py +483 -0
- edge_containers_cli-3.5.2/src/edge_containers_cli/cmds/monitor.tcss +51 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/docker.py +18 -2
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli.egg-info/PKG-INFO +1 -1
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/local.yaml +2 -2
- edge_containers_cli-3.5.0/src/edge_containers_cli/cmds/monitor.py +0 -285
- edge_containers_cli-3.5.0/src/edge_containers_cli/cmds/monitor.tcss +0 -45
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.copier-answers.yml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.devcontainer/devcontainer.json +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.github/CONTRIBUTING.md +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.github/actions/install_requirements/action.yml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.github/dependabot.yml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.github/pages/index.html +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.github/pages/make_switcher.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.github/workflows/_check.yml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.github/workflows/_dist.yml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.github/workflows/_pypi.yml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.github/workflows/_release.yml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.github/workflows/_sys_test.yml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.github/workflows/_test.yml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.github/workflows/_tox.yml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.github/workflows/ci.yml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.gitignore +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.pre-commit-config.yaml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.vscode/extensions.json +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.vscode/launch.json +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.vscode/settings.json +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/.vscode/tasks.json +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/Dockerfile +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/LICENSE +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/README.md +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/catalog-info.yaml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/conf.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/developer/explanations/decisions/0001-record-architecture-decisions.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/developer/explanations/decisions.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/developer/how-to/build-docs.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/developer/how-to/contribute.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/developer/how-to/lint.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/developer/how-to/make-release.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/developer/how-to/pin-requirements.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/developer/how-to/run-tests.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/developer/how-to/static-analysis.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/developer/how-to/test-container.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/developer/how-to/update-tools.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/developer/index.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/developer/reference/standards.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/developer/tutorials/dev-install.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/genindex.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/images/dls-favicon.ico +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/images/dls-logo.svg +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/index.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/user/explanations/docs-structure.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/user/how-to/run-container.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/user/index.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/user/reference/api.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/docs/user/tutorials/installation.rst +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/setup.cfg +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/__init__.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/__main__.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/autocomplete.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/cmds/__init__.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/cmds/helm.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/cmds/kubectl.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/git.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/globals.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/logging.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/shell.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/utils.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli.egg-info/SOURCES.txt +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli.egg-info/dependency_links.txt +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli.egg-info/entry_points.txt +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli.egg-info/requires.txt +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli.egg-info/top_level.txt +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/__init__.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/conftest.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/autocomplete.yaml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/.copier-answers.yml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/.github/dependabot.yml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/.github/workflows/ci_verify.sh +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/.github/workflows/verify.yml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/.gitignore +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/LICENSE +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/README.md +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/environment.sh +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/helm/shared/Chart.yaml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/helm/shared/README.md +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/helm/shared/values.yaml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/include/iocs/Chart.yaml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/include/iocs/templates/configmap.yaml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/services/bl01t-ea-test-01/Chart.yaml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/services/bl01t-ea-test-01/config/ioc.db +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/services/bl01t-ea-test-01/config/ioc.yaml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/services/bl01t-ea-test-01/pretend_helm.tgz +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/services/bl01t-ea-test-01/values.yaml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/bl01t/update-helm +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/data/ioc.yaml +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/test_autocomplete.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/test_cli.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/test_ioc.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/test_local.py +0 -0
- {edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/tests/test_system.py +0 -0
|
@@ -75,11 +75,15 @@ testpaths = "src tests"
|
|
|
75
75
|
|
|
76
76
|
[tool.coverage.run]
|
|
77
77
|
data_file = "/tmp/edge_containers_cli.coverage"
|
|
78
|
+
omit = ["src/edge_containers_cli/cmds/monitor.py"]
|
|
78
79
|
|
|
79
80
|
[tool.coverage.paths]
|
|
80
81
|
# Tests are run from installed location, map back to the src directory
|
|
81
82
|
source = ["src", "**/site-packages/"]
|
|
82
83
|
|
|
84
|
+
[tool.coverage.report]
|
|
85
|
+
omit = ["src/edge_containers_cli/cmds/monitor.py"]
|
|
86
|
+
|
|
83
87
|
# tox must currently be configured via an embedded ini string
|
|
84
88
|
# See: https://github.com/tox-dev/tox/issues/999
|
|
85
89
|
[tool.tox]
|
|
@@ -48,27 +48,27 @@ def commands(ctx):
|
|
|
48
48
|
@cli.command()
|
|
49
49
|
def ps(
|
|
50
50
|
ctx: typer.Context,
|
|
51
|
-
|
|
52
|
-
False, "-
|
|
51
|
+
running_only: bool = typer.Option(
|
|
52
|
+
False, "-r", "--running-only", help="list only IOCs/services that are running"
|
|
53
53
|
),
|
|
54
54
|
wide: bool = typer.Option(
|
|
55
55
|
False, "--wide", "-w", help="use a wide format with additional fields"
|
|
56
56
|
),
|
|
57
57
|
):
|
|
58
58
|
"""List the IOCs/services running in the current namespace"""
|
|
59
|
-
commands(ctx).ps(
|
|
59
|
+
commands(ctx).ps(running_only, wide)
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
@cli.command()
|
|
63
63
|
def monitor(
|
|
64
64
|
ctx: typer.Context,
|
|
65
|
-
|
|
66
|
-
False, "-
|
|
65
|
+
running_only: bool = typer.Option(
|
|
66
|
+
False, "-r", "--running-only", help="list only IOCs/services that are running"
|
|
67
67
|
),
|
|
68
68
|
):
|
|
69
69
|
"""Open IOC monitor TUI."""
|
|
70
70
|
cmds = commands(ctx)
|
|
71
|
-
app = MonitorApp(EC_K8S_NAMESPACE, cmds,
|
|
71
|
+
app = MonitorApp(EC_K8S_NAMESPACE, cmds, running_only)
|
|
72
72
|
app.run()
|
|
73
73
|
|
|
74
74
|
|
{edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/cmds/commands.py
RENAMED
|
@@ -42,7 +42,7 @@ class Commands:
|
|
|
42
42
|
def exec(self, service_name: str):
|
|
43
43
|
raise NotImplementedError
|
|
44
44
|
|
|
45
|
-
def logs(self, service_name: str, prev: bool, follow: bool):
|
|
45
|
+
def logs(self, service_name: str, prev: bool, follow: bool, stdout: bool):
|
|
46
46
|
raise NotImplementedError
|
|
47
47
|
|
|
48
48
|
def restart(self, service_name: str):
|
|
@@ -54,11 +54,11 @@ class Commands:
|
|
|
54
54
|
def stop(self, service_name: str):
|
|
55
55
|
raise NotImplementedError
|
|
56
56
|
|
|
57
|
-
def get_services(self,
|
|
57
|
+
def get_services(self, running_only: bool) -> polars.DataFrame:
|
|
58
58
|
raise NotImplementedError
|
|
59
59
|
|
|
60
|
-
def ps(self,
|
|
61
|
-
select_data = self.get_services(
|
|
60
|
+
def ps(self, running_only: bool, wide: bool):
|
|
61
|
+
select_data = self.get_services(running_only)
|
|
62
62
|
if not wide:
|
|
63
63
|
select_data.drop_in_place("image")
|
|
64
64
|
print(select_data)
|
{edge_containers_cli-3.5.0 → edge_containers_cli-3.5.2}/src/edge_containers_cli/cmds/k8s_commands.py
RENAMED
|
@@ -7,7 +7,7 @@ Relies on the Helm class for deployment aspects.
|
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from io import StringIO
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Optional
|
|
10
|
+
from typing import Optional, Union
|
|
11
11
|
|
|
12
12
|
import polars
|
|
13
13
|
import typer
|
|
@@ -123,14 +123,25 @@ class K8sCommands(Commands):
|
|
|
123
123
|
fullname = check_service(service_name, self.namespace)
|
|
124
124
|
shell.run_command(f"kubectl -it -n {self.namespace} exec {fullname} -- bash")
|
|
125
125
|
|
|
126
|
-
def logs(
|
|
126
|
+
def logs(
|
|
127
|
+
self, service_name: str, prev: bool, follow: bool, stdout: bool = False
|
|
128
|
+
) -> Optional[Union[str, bool]]:
|
|
127
129
|
fullname = check_service(service_name, self.namespace)
|
|
128
130
|
previous = "-p" if prev else ""
|
|
129
131
|
fol = "-f" if follow else ""
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
if stdout:
|
|
134
|
+
a = shell.run_command(
|
|
135
|
+
f"kubectl -n {self.namespace} logs {fullname} {previous} {fol}",
|
|
136
|
+
interactive=False,
|
|
137
|
+
show=False,
|
|
138
|
+
error_OK=True,
|
|
139
|
+
)
|
|
140
|
+
return a
|
|
141
|
+
else:
|
|
142
|
+
shell.run_command(
|
|
143
|
+
f"kubectl -n {self.namespace} logs {fullname} {previous} {fol}",
|
|
144
|
+
)
|
|
134
145
|
|
|
135
146
|
def restart(self, service_name):
|
|
136
147
|
check_service(service_name, self.namespace)
|
|
@@ -149,7 +160,7 @@ class K8sCommands(Commands):
|
|
|
149
160
|
"""Stop an IOC"""
|
|
150
161
|
shell.run_command(f"kubectl scale -n {self.namespace} {fullname} --replicas=0 ")
|
|
151
162
|
|
|
152
|
-
def get_services(self,
|
|
163
|
+
def get_services(self, running_only: bool) -> polars.DataFrame:
|
|
153
164
|
services_df = polars.DataFrame()
|
|
154
165
|
|
|
155
166
|
# Gives all services (running & not running) and their image
|
|
@@ -188,7 +199,7 @@ class K8sCommands(Commands):
|
|
|
188
199
|
polars.col("running").replace({"Running": True}, default=False),
|
|
189
200
|
polars.col("restarts").fill_null(0),
|
|
190
201
|
)
|
|
191
|
-
elif
|
|
202
|
+
elif not running_only:
|
|
192
203
|
services_df = services_df.with_columns(
|
|
193
204
|
running=polars.lit(False), restarts=polars.lit(0)
|
|
194
205
|
)
|
|
@@ -210,14 +221,14 @@ class K8sCommands(Commands):
|
|
|
210
221
|
services_df = services_df.select(
|
|
211
222
|
["name", "version", "running", "restarts", "deployed", "image"]
|
|
212
223
|
)
|
|
213
|
-
if
|
|
224
|
+
if running_only:
|
|
214
225
|
services_df = services_df.filter(polars.col("running").eq(True))
|
|
215
226
|
log.debug(services_df)
|
|
216
227
|
return services_df
|
|
217
228
|
|
|
218
|
-
def ps(self,
|
|
229
|
+
def ps(self, running_only: bool, wide: bool):
|
|
219
230
|
"""List all IOCs and Services in the current namespace"""
|
|
220
|
-
services_df = self.get_services(
|
|
231
|
+
services_df = self.get_services(running_only)
|
|
221
232
|
if not wide:
|
|
222
233
|
services_df.drop_in_place("image")
|
|
223
234
|
log.debug(services_df)
|
|
@@ -141,8 +141,8 @@ class LocalCommands(Commands):
|
|
|
141
141
|
def exec(self, service_name):
|
|
142
142
|
self.docker.exec(service_name, "bash", args="-it")
|
|
143
143
|
|
|
144
|
-
def logs(self, service_name: str, prev: bool, follow: bool):
|
|
145
|
-
self.docker.logs(service_name, prev, follow)
|
|
144
|
+
def logs(self, service_name: str, prev: bool, follow: bool, stdout: bool = False):
|
|
145
|
+
self.docker.logs(service_name, prev, follow, stdout)
|
|
146
146
|
|
|
147
147
|
def restart(self, service_name: str):
|
|
148
148
|
shell.run_command(f"{self.docker.docker} restart {service_name}")
|
|
@@ -153,8 +153,8 @@ class LocalCommands(Commands):
|
|
|
153
153
|
def stop(self, service_name: str):
|
|
154
154
|
shell.run_command(f"{self.docker.docker} stop {service_name}")
|
|
155
155
|
|
|
156
|
-
def get_services(self,
|
|
157
|
-
all_arg = " --all" if
|
|
156
|
+
def get_services(self, running_only: bool) -> polars.DataFrame:
|
|
157
|
+
all_arg = " --all" if not running_only else ""
|
|
158
158
|
|
|
159
159
|
# List services
|
|
160
160
|
avail_services = shell.run_command(
|
|
@@ -201,8 +201,8 @@ class LocalCommands(Commands):
|
|
|
201
201
|
log.debug(select_data)
|
|
202
202
|
return polars.DataFrame(select_data)
|
|
203
203
|
|
|
204
|
-
def ps(self,
|
|
205
|
-
select_data = self.get_services(
|
|
204
|
+
def ps(self, running_only: bool, wide: bool):
|
|
205
|
+
select_data = self.get_services(running_only)
|
|
206
206
|
services_df = polars.DataFrame(select_data)
|
|
207
207
|
if not wide:
|
|
208
208
|
services_df.drop_in_place("image")
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""TUI monitor for containerised IOCs."""
|
|
2
|
+
|
|
3
|
+
from functools import total_ordering
|
|
4
|
+
from threading import Thread
|
|
5
|
+
from time import sleep
|
|
6
|
+
from typing import Any, Callable, Union, cast
|
|
7
|
+
|
|
8
|
+
import polars
|
|
9
|
+
from rich.style import Style
|
|
10
|
+
from rich.syntax import Syntax
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
# from textual import on
|
|
14
|
+
from textual import work
|
|
15
|
+
from textual.app import App, ComposeResult
|
|
16
|
+
from textual.binding import Binding
|
|
17
|
+
from textual.color import Color
|
|
18
|
+
from textual.containers import Grid
|
|
19
|
+
from textual.reactive import reactive
|
|
20
|
+
from textual.screen import ModalScreen
|
|
21
|
+
from textual.widget import Widget
|
|
22
|
+
from textual.widgets import Button, DataTable, Footer, Header, Label, RichLog
|
|
23
|
+
from textual.widgets.data_table import RowKey
|
|
24
|
+
|
|
25
|
+
from edge_containers_cli.cmds.commands import Commands
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OptionScreen(ModalScreen[bool], inherit_bindings=False):
|
|
29
|
+
BINDINGS = [
|
|
30
|
+
Binding("y,enter", "option_yes", "Yes"),
|
|
31
|
+
Binding("n,c,escape", "option_cancel", "Cancel"),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
def __init__(self, service_name: str) -> None:
|
|
35
|
+
super().__init__()
|
|
36
|
+
|
|
37
|
+
self.service_name = service_name
|
|
38
|
+
self.type_action = "stop"
|
|
39
|
+
|
|
40
|
+
def compose(self) -> ComposeResult:
|
|
41
|
+
yield Grid(
|
|
42
|
+
Label(
|
|
43
|
+
f"Are you sure you want to {self.type_action} {self.service_name}?",
|
|
44
|
+
id="question",
|
|
45
|
+
),
|
|
46
|
+
Button("Yes", variant="error", id="yes"),
|
|
47
|
+
Button("No", variant="primary", id="cancel"),
|
|
48
|
+
id="dialog",
|
|
49
|
+
)
|
|
50
|
+
yield Footer()
|
|
51
|
+
|
|
52
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
53
|
+
if event.button.id == "yes":
|
|
54
|
+
self.action_option_yes()
|
|
55
|
+
else:
|
|
56
|
+
self.action_option_cancel()
|
|
57
|
+
|
|
58
|
+
def action_option_yes(self) -> None:
|
|
59
|
+
self.dismiss(True)
|
|
60
|
+
|
|
61
|
+
def action_option_cancel(self) -> None:
|
|
62
|
+
self.dismiss(False)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class StartScreen(OptionScreen):
|
|
66
|
+
"""Screen with dialog to start service."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, service_name: str) -> None:
|
|
69
|
+
super().__init__(service_name)
|
|
70
|
+
|
|
71
|
+
self.type_action = "start"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class StopScreen(OptionScreen):
|
|
75
|
+
"""Screen with dialog to stop service."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, service_name: str) -> None:
|
|
78
|
+
super().__init__(service_name)
|
|
79
|
+
|
|
80
|
+
self.type_action = "stop"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class RestartScreen(OptionScreen):
|
|
84
|
+
"""Screen with dialog to restart service."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, service_name: str) -> None:
|
|
87
|
+
super().__init__(service_name)
|
|
88
|
+
|
|
89
|
+
self.type_action = "restart"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class LogsScreen(ModalScreen, inherit_bindings=False):
|
|
93
|
+
"""Screen to display IOC logs."""
|
|
94
|
+
|
|
95
|
+
BINDINGS = [
|
|
96
|
+
Binding("q", "close_screen", "Close"),
|
|
97
|
+
Binding("up,w,k", "scroll_up", "Scroll Up", show=False),
|
|
98
|
+
Binding("down,s,j", "scroll_down", "Scroll Down", show=False),
|
|
99
|
+
Binding("left,h", "scroll_left", "Scroll Left", show=False),
|
|
100
|
+
Binding("right,l", "scroll_right", "Scroll Right", show=False),
|
|
101
|
+
Binding("home,G", "scroll_home", "Scroll Home", show=False),
|
|
102
|
+
Binding("end,g", "scroll_end", "Scroll End", show=False),
|
|
103
|
+
Binding("pageup,b", "page_up", "Page Up", show=False),
|
|
104
|
+
Binding("pagedown,space", "page_down", "Page Down", show=False),
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
def __init__(self, fetch_log: Callable, service_name) -> None:
|
|
108
|
+
super().__init__()
|
|
109
|
+
|
|
110
|
+
self.fetch_log = fetch_log
|
|
111
|
+
self.service_name = service_name
|
|
112
|
+
self.log_text = ""
|
|
113
|
+
|
|
114
|
+
def compose(self) -> ComposeResult:
|
|
115
|
+
yield RichLog(highlight=True, id="log")
|
|
116
|
+
yield Footer()
|
|
117
|
+
|
|
118
|
+
def on_mount(self) -> None:
|
|
119
|
+
log = self.query_one(RichLog)
|
|
120
|
+
log.loading = True
|
|
121
|
+
self.load_logs(log)
|
|
122
|
+
|
|
123
|
+
@work
|
|
124
|
+
async def load_logs(self, log: RichLog) -> None:
|
|
125
|
+
self.log_text: str = self.fetch_log(
|
|
126
|
+
self.service_name, prev=False, follow=False, stdout=True
|
|
127
|
+
)
|
|
128
|
+
log.loading = False
|
|
129
|
+
width = max(len(line) for line in self.log_text.split("\n"))
|
|
130
|
+
log.write(
|
|
131
|
+
Syntax(self.log_text, "bash", line_numbers=True),
|
|
132
|
+
width=width + 10,
|
|
133
|
+
expand=True,
|
|
134
|
+
shrink=False,
|
|
135
|
+
scroll_end=True,
|
|
136
|
+
)
|
|
137
|
+
log.focus()
|
|
138
|
+
|
|
139
|
+
def action_close_screen(self) -> None:
|
|
140
|
+
self.app.pop_screen()
|
|
141
|
+
|
|
142
|
+
def action_scroll_up(self) -> None:
|
|
143
|
+
log = self.query_one(RichLog)
|
|
144
|
+
log.action_scroll_up()
|
|
145
|
+
|
|
146
|
+
def action_scroll_down(self) -> None:
|
|
147
|
+
log = self.query_one(RichLog)
|
|
148
|
+
log.action_scroll_down()
|
|
149
|
+
|
|
150
|
+
def action_scroll_home(self) -> None:
|
|
151
|
+
log = self.query_one(RichLog)
|
|
152
|
+
log.action_scroll_home()
|
|
153
|
+
|
|
154
|
+
def action_scroll_end(self) -> None:
|
|
155
|
+
log = self.query_one(RichLog)
|
|
156
|
+
log.action_scroll_end()
|
|
157
|
+
|
|
158
|
+
def action_page_down(self) -> None:
|
|
159
|
+
log = self.query_one(RichLog)
|
|
160
|
+
log.action_page_down()
|
|
161
|
+
|
|
162
|
+
def action_page_up(self) -> None:
|
|
163
|
+
log = self.query_one(RichLog)
|
|
164
|
+
log.action_page_up()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@total_ordering
|
|
168
|
+
class SortableText(Text):
|
|
169
|
+
__slots__ = ("value",)
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
value: Any,
|
|
174
|
+
text: str,
|
|
175
|
+
color: Color,
|
|
176
|
+
justify: Any = "left", # "Any" is a hack: justify should be Literal
|
|
177
|
+
) -> None:
|
|
178
|
+
self.value = value
|
|
179
|
+
super().__init__(
|
|
180
|
+
str(text),
|
|
181
|
+
Style(color=color.rich_color),
|
|
182
|
+
justify=justify,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def __lt__(self, other: Any) -> bool:
|
|
186
|
+
if type(other) != SortableText:
|
|
187
|
+
return NotImplemented
|
|
188
|
+
return cast(bool, self.value < other.value)
|
|
189
|
+
|
|
190
|
+
def __gt__(self, other: Any) -> bool:
|
|
191
|
+
if type(other) != SortableText:
|
|
192
|
+
return NotImplemented
|
|
193
|
+
return cast(bool, self.value > other.value)
|
|
194
|
+
|
|
195
|
+
def __eq__(self, other: Any) -> bool:
|
|
196
|
+
if type(other) != SortableText:
|
|
197
|
+
return NotImplemented
|
|
198
|
+
return cast(bool, self.value == other.value)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class IocTable(Widget):
|
|
202
|
+
"""Widget to display the IOC table."""
|
|
203
|
+
|
|
204
|
+
default_sort_column_id = "name"
|
|
205
|
+
# init=False otherwise triggers table query before yielded in compose
|
|
206
|
+
sort_column_id = reactive(default_sort_column_id, init=False)
|
|
207
|
+
|
|
208
|
+
def __init__(self, commands, running_only: bool) -> None:
|
|
209
|
+
super().__init__()
|
|
210
|
+
|
|
211
|
+
self.commands = commands
|
|
212
|
+
self.running_only = running_only
|
|
213
|
+
self.iocs_df = self.commands.get_services(self.running_only)
|
|
214
|
+
|
|
215
|
+
self._polling = True
|
|
216
|
+
self._poll_thread = Thread(target=self._poll_services)
|
|
217
|
+
self._poll_thread.start()
|
|
218
|
+
self._get_iocs()
|
|
219
|
+
|
|
220
|
+
def _poll_services(self):
|
|
221
|
+
while self._polling:
|
|
222
|
+
# ioc list data table update loop
|
|
223
|
+
print()
|
|
224
|
+
self.iocs_df = self.commands.get_services(self.running_only)
|
|
225
|
+
sleep(2.0)
|
|
226
|
+
|
|
227
|
+
def stop(self):
|
|
228
|
+
self._polling = False
|
|
229
|
+
self._poll_thread.join()
|
|
230
|
+
|
|
231
|
+
def _get_iocs(self) -> None:
|
|
232
|
+
iocs = self._convert_df_to_list(self.iocs_df)
|
|
233
|
+
# give up the GIL to other threads
|
|
234
|
+
sleep(0)
|
|
235
|
+
self.iocs = sorted(iocs, key=lambda d: d["name"])
|
|
236
|
+
exclude = ["deployed", "image"]
|
|
237
|
+
|
|
238
|
+
for i, ioc in enumerate(self.iocs):
|
|
239
|
+
ioc = {key: value for key, value in ioc.items() if key not in exclude}
|
|
240
|
+
self.iocs[i] = ioc
|
|
241
|
+
self.columns = [key for key in self.iocs[0].keys() if key not in exclude]
|
|
242
|
+
|
|
243
|
+
def _convert_df_to_list(self, iocs_df: Union[polars.DataFrame, list]) -> list[dict]:
|
|
244
|
+
if isinstance(iocs_df, polars.DataFrame):
|
|
245
|
+
iocs = iocs_df.to_dicts()
|
|
246
|
+
else:
|
|
247
|
+
iocs = iocs_df
|
|
248
|
+
|
|
249
|
+
return iocs
|
|
250
|
+
|
|
251
|
+
def on_mount(self) -> None:
|
|
252
|
+
"""Provides a loop after generating the app for updating the data."""
|
|
253
|
+
self.set_interval(1.0, self.update_iocs)
|
|
254
|
+
|
|
255
|
+
async def update_iocs(self) -> None:
|
|
256
|
+
"""Updates the IOC stats data."""
|
|
257
|
+
# Fetch services dataframe
|
|
258
|
+
self._get_iocs()
|
|
259
|
+
|
|
260
|
+
await self.populate_table()
|
|
261
|
+
|
|
262
|
+
def _get_heading(self, column_id: str):
|
|
263
|
+
sorted_style = Style(bold=True, underline=True)
|
|
264
|
+
|
|
265
|
+
if column_id == self.sort_column_id:
|
|
266
|
+
heading = Text(column_id, justify="center", style=sorted_style)
|
|
267
|
+
else:
|
|
268
|
+
# screen.sort() is referring to the screen action function
|
|
269
|
+
heading = Text(column_id, justify="center").on(
|
|
270
|
+
click=f"app.sort('{column_id}')"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return heading
|
|
274
|
+
|
|
275
|
+
def compose(self) -> ComposeResult:
|
|
276
|
+
table: DataTable[Text] = DataTable(
|
|
277
|
+
id="body_table", header_height=1, show_cursor=False, zebra_stripes=True
|
|
278
|
+
)
|
|
279
|
+
table.focus()
|
|
280
|
+
|
|
281
|
+
for column_id in self.columns:
|
|
282
|
+
heading = self._get_heading(column_id)
|
|
283
|
+
table.add_column(heading, key=str(column_id))
|
|
284
|
+
|
|
285
|
+
# Set a size for the left column
|
|
286
|
+
# table.ordered_columns[0].content_width = 50
|
|
287
|
+
|
|
288
|
+
table.show_cursor = True
|
|
289
|
+
table.cursor_type = "row"
|
|
290
|
+
|
|
291
|
+
yield table
|
|
292
|
+
|
|
293
|
+
def watch_sort_column_id(self, sort_column_id: str) -> None:
|
|
294
|
+
"""Called when the sort_column_id attribute changes."""
|
|
295
|
+
table = self.query_one("#body_table", DataTable)
|
|
296
|
+
|
|
297
|
+
# Reformat headings based on new sorted column
|
|
298
|
+
for i, _column in enumerate(self.columns):
|
|
299
|
+
table.ordered_columns[i].label = self._get_heading(_column)
|
|
300
|
+
|
|
301
|
+
sorted_col = self.columns.index(sort_column_id)
|
|
302
|
+
|
|
303
|
+
table.sort(table.ordered_columns[sorted_col].key, reverse=False)
|
|
304
|
+
|
|
305
|
+
def _get_color(self, value: str) -> Color:
|
|
306
|
+
if value == "True":
|
|
307
|
+
return Color.parse("lime")
|
|
308
|
+
elif value == "False":
|
|
309
|
+
return Color.parse("red")
|
|
310
|
+
else:
|
|
311
|
+
return Color.parse("white")
|
|
312
|
+
|
|
313
|
+
async def populate_table(self) -> None:
|
|
314
|
+
"""Method to render the TUI table."""
|
|
315
|
+
table = self.query_one("#body_table", DataTable)
|
|
316
|
+
|
|
317
|
+
if not table.columns:
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
iocs = set(table.rows)
|
|
321
|
+
new_iocs = set()
|
|
322
|
+
|
|
323
|
+
# For each IOC row
|
|
324
|
+
for ioc in self.iocs:
|
|
325
|
+
row_key = str(ioc["name"])
|
|
326
|
+
new_iocs.add(RowKey(row_key))
|
|
327
|
+
|
|
328
|
+
cells = [
|
|
329
|
+
{
|
|
330
|
+
"col_key": key,
|
|
331
|
+
"contents": SortableText(
|
|
332
|
+
ioc[key], str(ioc[key]), self._get_color(str(ioc[key]))
|
|
333
|
+
),
|
|
334
|
+
}
|
|
335
|
+
for key in self.columns
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
if row_key not in table.rows:
|
|
339
|
+
table.add_row(
|
|
340
|
+
*[cell["contents"] for cell in cells],
|
|
341
|
+
key=row_key,
|
|
342
|
+
)
|
|
343
|
+
else:
|
|
344
|
+
for cell in cells:
|
|
345
|
+
table.update_cell(row_key, cell["col_key"], cell["contents"])
|
|
346
|
+
|
|
347
|
+
# If any IOC has been removed, remove it from the table
|
|
348
|
+
for old_row_key in iocs - new_iocs:
|
|
349
|
+
table.remove_row(old_row_key)
|
|
350
|
+
|
|
351
|
+
# Sort by column
|
|
352
|
+
table.sort(self.sort_column_id, reverse=False)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class MonitorApp(App):
|
|
356
|
+
def __init__(
|
|
357
|
+
self,
|
|
358
|
+
beamline: str,
|
|
359
|
+
commands: Commands,
|
|
360
|
+
running_only: bool,
|
|
361
|
+
) -> None:
|
|
362
|
+
super().__init__()
|
|
363
|
+
|
|
364
|
+
self.commands = commands
|
|
365
|
+
self.running_only = running_only
|
|
366
|
+
self.beamline = beamline
|
|
367
|
+
|
|
368
|
+
CSS_PATH = "monitor.tcss"
|
|
369
|
+
|
|
370
|
+
BINDINGS = [
|
|
371
|
+
Binding("escape", "close_application", "Exit"),
|
|
372
|
+
Binding("s", "start_ioc", "Start IOC"),
|
|
373
|
+
Binding("t", "stop_ioc", "Stop IOC"),
|
|
374
|
+
Binding("r", "restart_ioc", "Restart IOC"),
|
|
375
|
+
Binding("l", "ioc_logs", "IOC Logs"),
|
|
376
|
+
Binding("o", "sort", "Sort"),
|
|
377
|
+
# Binding("d", "toggle_dark", "Toggle dark mode"),
|
|
378
|
+
]
|
|
379
|
+
|
|
380
|
+
def compose(self) -> ComposeResult:
|
|
381
|
+
"""Create child widgets for the app."""
|
|
382
|
+
yield Header(show_clock=True)
|
|
383
|
+
self.table = IocTable(self.commands, self.running_only)
|
|
384
|
+
yield self.table
|
|
385
|
+
yield Footer()
|
|
386
|
+
|
|
387
|
+
def on_mount(self) -> None:
|
|
388
|
+
self.title = f"{self.beamline} IOC Monitor"
|
|
389
|
+
|
|
390
|
+
def on_unmount(self) -> None:
|
|
391
|
+
"""Executes when the app is closed."""
|
|
392
|
+
# Makes sure the thread is stopped even if the App crashes
|
|
393
|
+
self.table.stop()
|
|
394
|
+
|
|
395
|
+
# def action_toggle_dark(self) -> None:
|
|
396
|
+
# """An action to toggle dark mode."""
|
|
397
|
+
# self.dark = not self.dark
|
|
398
|
+
|
|
399
|
+
def action_close_application(self) -> None:
|
|
400
|
+
"""Provide another way of exiting the app along with CTRL+C."""
|
|
401
|
+
self.exit()
|
|
402
|
+
|
|
403
|
+
def _get_highlighted_cell(self, col_key: str) -> str:
|
|
404
|
+
table = self.get_widget_by_id("body_table")
|
|
405
|
+
assert isinstance(table, DataTable)
|
|
406
|
+
# Fetches hightlighted row ID (integer)
|
|
407
|
+
row = table.cursor_row
|
|
408
|
+
ioc_row = table.ordered_rows[row]
|
|
409
|
+
col_keys = [ord_col.key.value for ord_col in table.ordered_columns]
|
|
410
|
+
col_i = col_keys.index(col_key)
|
|
411
|
+
ioc_col = table.ordered_columns[col_i]
|
|
412
|
+
cell: Union[str, SortableText] = table.get_cell(ioc_row.key, ioc_col.key)
|
|
413
|
+
# SortableText inherits __str__() from Text
|
|
414
|
+
return str(cell)
|
|
415
|
+
|
|
416
|
+
def _get_service_name(self) -> str:
|
|
417
|
+
service_name = self._get_highlighted_cell("name")
|
|
418
|
+
return service_name
|
|
419
|
+
|
|
420
|
+
def action_start_ioc(self) -> None:
|
|
421
|
+
"""Start the IOC that is currently highlighted."""
|
|
422
|
+
service_name = self._get_service_name()
|
|
423
|
+
|
|
424
|
+
def check_start(restart: bool) -> None:
|
|
425
|
+
"""Called when StartScreen is dismissed."""
|
|
426
|
+
if restart:
|
|
427
|
+
self.commands.start(service_name)
|
|
428
|
+
|
|
429
|
+
self.push_screen(StartScreen(service_name), check_start)
|
|
430
|
+
|
|
431
|
+
def action_stop_ioc(self) -> None:
|
|
432
|
+
"""Stop the IOC that is currently highlighted."""
|
|
433
|
+
service_name = self._get_service_name()
|
|
434
|
+
|
|
435
|
+
def check_stop(restart: bool) -> None:
|
|
436
|
+
"""Called when StopScreen is dismissed."""
|
|
437
|
+
if restart:
|
|
438
|
+
self.commands.stop(service_name)
|
|
439
|
+
|
|
440
|
+
self.push_screen(StopScreen(service_name), check_stop)
|
|
441
|
+
|
|
442
|
+
def action_restart_ioc(self) -> None:
|
|
443
|
+
"""Restart the IOC that is currently highlighted."""
|
|
444
|
+
service_name = self._get_service_name()
|
|
445
|
+
|
|
446
|
+
def check_restart(restart: bool) -> None:
|
|
447
|
+
"""Called when RestartScreen is dismissed."""
|
|
448
|
+
if restart:
|
|
449
|
+
self.commands.restart(service_name)
|
|
450
|
+
|
|
451
|
+
self.push_screen(RestartScreen(service_name), check_restart)
|
|
452
|
+
|
|
453
|
+
def action_ioc_logs(self) -> None:
|
|
454
|
+
"""Display the logs of the IOC that is currently highlighted."""
|
|
455
|
+
service_name = self._get_service_name()
|
|
456
|
+
|
|
457
|
+
# Convert to corresponding bool
|
|
458
|
+
running = self._get_highlighted_cell("running") == "True"
|
|
459
|
+
|
|
460
|
+
if running:
|
|
461
|
+
command = self.commands.logs
|
|
462
|
+
self.push_screen(LogsScreen(command, service_name))
|
|
463
|
+
|
|
464
|
+
def action_sort(self, col_name: str = "") -> None:
|
|
465
|
+
"""An action to sort the table rows by column heading."""
|
|
466
|
+
if col_name != "":
|
|
467
|
+
# If col_name is provided, sort by that column
|
|
468
|
+
# e.g. if a column heading is clicked
|
|
469
|
+
new_col = col_name
|
|
470
|
+
else:
|
|
471
|
+
# If no column name is provided (e.g. by pressing the key bind),
|
|
472
|
+
# then just cycle to the next column
|
|
473
|
+
table = self.query_one(IocTable)
|
|
474
|
+
col_name = table.sort_column_id
|
|
475
|
+
cols = table.columns
|
|
476
|
+
col_index = cols.index(col_name)
|
|
477
|
+
new_col = cols[0 if col_index + 1 > 3 else col_index + 1]
|
|
478
|
+
self.update_sort_key(new_col)
|
|
479
|
+
|
|
480
|
+
def update_sort_key(self, col_name: str) -> None:
|
|
481
|
+
"""Method called to update the table sort key attribute."""
|
|
482
|
+
table = self.query_one(IocTable)
|
|
483
|
+
table.sort_column_id = col_name
|