rtems-proxy 0.2.4__tar.gz → 0.3.0__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.
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/PKG-INFO +5 -2
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/proxy-start.sh +7 -7
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/pyproject.toml +6 -1
- rtems-proxy-0.3.0/src/rtems_proxy/__main__.py +149 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy/_version.py +2 -2
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy/copy.py +20 -14
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy/globals.py +1 -1
- rtems-proxy-0.3.0/src/rtems_proxy/rsync.sh.jinja +31 -0
- rtems-proxy-0.3.0/src/rtems_proxy/telnet.py +202 -0
- rtems-proxy-0.3.0/src/rtems_proxy/utils.py +49 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy.egg-info/PKG-INFO +5 -2
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy.egg-info/SOURCES.txt +2 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy.egg-info/requires.txt +4 -1
- rtems-proxy-0.2.4/src/rtems_proxy/__main__.py +0 -71
- rtems-proxy-0.2.4/src/rtems_proxy/telnet.py +0 -117
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.copier-answers.yml +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.devcontainer/devcontainer.json +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/CONTRIBUTING.md +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/actions/install_requirements/action.yml +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/dependabot.yml +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/pages/index.html +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/pages/make_switcher.py +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/workflows/_check.yml +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/workflows/_dist.yml +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/workflows/_pypi.yml +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/workflows/_release.yml +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/workflows/_test.yml +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/workflows/_tox.yml +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/workflows/ci.yml +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.gitignore +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.pre-commit-config.yaml +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.vscode/extensions.json +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.vscode/launch.json +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.vscode/settings.json +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.vscode/tasks.json +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/Dockerfile +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/LICENSE +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/README.md +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/catalog-info.yaml +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/requirements.txt +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/setup.cfg +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy/__init__.py +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy.egg-info/dependency_links.txt +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy.egg-info/entry_points.txt +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy.egg-info/top_level.txt +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/tests/conftest.py +0 -0
- {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: rtems-proxy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Support for a K8S proxy container in controlling and monitoring RTEMS EPICS IOCs
|
|
5
5
|
Author-email: Giles Knap <giles.knap@diamond.ac.uk>
|
|
6
6
|
License: Apache License
|
|
@@ -216,8 +216,11 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
216
216
|
Requires-Python: >=3.7
|
|
217
217
|
Description-Content-Type: text/markdown
|
|
218
218
|
License-File: LICENSE
|
|
219
|
-
Requires-Dist:
|
|
219
|
+
Requires-Dist: jinja2
|
|
220
|
+
Requires-Dist: pexpect
|
|
221
|
+
Requires-Dist: ruamel.yaml
|
|
220
222
|
Requires-Dist: telnetlib3
|
|
223
|
+
Requires-Dist: typer
|
|
221
224
|
Provides-Extra: dev
|
|
222
225
|
Requires-Dist: copier; extra == "dev"
|
|
223
226
|
Requires-Dist: mypy; extra == "dev"
|
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
set -x
|
|
4
4
|
|
|
5
5
|
# This is the folder the PVC for the nfsv2tftp shared volume is mounted into.
|
|
6
|
-
export
|
|
6
|
+
export RTEMS_TFTP_PATH=${RTEMS_TFTP_PATH:-/nfsv2-tftp}
|
|
7
7
|
|
|
8
|
-
if [ ! -d ${
|
|
8
|
+
if [ ! -d ${RTEMS_TFTP_PATH} ]; then
|
|
9
9
|
echo "ERROR: No PVC folder found."
|
|
10
10
|
# make a folder for testing outside of the cluster
|
|
11
|
-
mkdir -p ${
|
|
11
|
+
mkdir -p ${RTEMS_TFTP_PATH}
|
|
12
12
|
fi
|
|
13
13
|
|
|
14
14
|
# copy the IOC instance's runtime assets into the shared volume
|
|
15
|
-
cp -rL /epics/ioc ${
|
|
16
|
-
cp -r /epics/runtime ${
|
|
15
|
+
cp -rL /epics/ioc ${RTEMS_TFTP_PATH}
|
|
16
|
+
cp -r /epics/runtime ${RTEMS_TFTP_PATH}
|
|
17
17
|
# move binary to the root for shorter paths
|
|
18
|
-
mv ${
|
|
18
|
+
mv ${RTEMS_TFTP_PATH}/ioc/bin/*/ioc.boot ${RTEMS_TFTP_PATH}
|
|
19
19
|
# fix up the paths in st.cmd
|
|
20
|
-
sed -i "s|/epics/|/iocs/${IOC_LOCATION}/${IOC_NAME}/|" ${
|
|
20
|
+
sed -i "s|/epics/|/iocs/${IOC_LOCATION}/${IOC_NAME}/|" ${RTEMS_TFTP_PATH}/runtime/st.cmd
|
|
21
21
|
|
|
22
22
|
# keep the container running ...
|
|
23
23
|
while true; do
|
|
@@ -14,7 +14,7 @@ classifiers = [
|
|
|
14
14
|
"Programming Language :: Python :: 3.11",
|
|
15
15
|
]
|
|
16
16
|
description = "Support for a K8S proxy container in controlling and monitoring RTEMS EPICS IOCs"
|
|
17
|
-
dependencies = ["
|
|
17
|
+
dependencies = ["jinja2", "pexpect", "ruamel.yaml", "telnetlib3", "typer"]
|
|
18
18
|
dynamic = ["version"]
|
|
19
19
|
license.file = "LICENSE"
|
|
20
20
|
readme = "README.md"
|
|
@@ -89,6 +89,11 @@ commands =
|
|
|
89
89
|
"""
|
|
90
90
|
|
|
91
91
|
[tool.ruff]
|
|
92
|
+
ignore = [
|
|
93
|
+
"B008", # Do not perform unnecessary work in __all__
|
|
94
|
+
"C408", # Unnecessary collection call - e.g. list(...) instead of [...]
|
|
95
|
+
"E501", # Line too long, should be fixed by black.
|
|
96
|
+
]
|
|
92
97
|
src = ["src", "tests"]
|
|
93
98
|
line-length = 88
|
|
94
99
|
lint.select = [
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from time import sleep
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from jinja2 import Template
|
|
7
|
+
from ruamel.yaml import YAML
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
from .copy import copy_rtems
|
|
11
|
+
from .globals import GLOBALS
|
|
12
|
+
from .telnet import ioc_connect
|
|
13
|
+
|
|
14
|
+
__all__ = ["main"]
|
|
15
|
+
|
|
16
|
+
cli = typer.Typer()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def version_callback(value: bool):
|
|
20
|
+
if value:
|
|
21
|
+
typer.echo(__version__)
|
|
22
|
+
raise typer.Exit()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@cli.callback()
|
|
26
|
+
def main(
|
|
27
|
+
version: Optional[bool] = typer.Option(
|
|
28
|
+
None,
|
|
29
|
+
"--version",
|
|
30
|
+
callback=version_callback,
|
|
31
|
+
is_eager=True,
|
|
32
|
+
help="Print the version of ibek and exit",
|
|
33
|
+
),
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
Proxy for RTEMS IOCs controlling and monitoring
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@cli.command()
|
|
41
|
+
def start(
|
|
42
|
+
copy: bool = typer.Option(
|
|
43
|
+
True, "--copy/--no-copy", help="copy binaries before connecting"
|
|
44
|
+
),
|
|
45
|
+
reboot: bool = typer.Option(
|
|
46
|
+
True, "--reboot/--no-reboot", help="reboot the IOC first"
|
|
47
|
+
),
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
Starts an RTEMS IOC. Places the IOC binaries in the expected location,
|
|
51
|
+
restarts the IOC and connects stdio to the IOC console.
|
|
52
|
+
|
|
53
|
+
This should be called inside of a runtime IOC container after ibek
|
|
54
|
+
has generated the runtime assets for the IOC.
|
|
55
|
+
|
|
56
|
+
The standard 'start.sh' in the runtime IOC will call this entry point if
|
|
57
|
+
it detects that EPICS_HOST_ARCH==RTEMS-beatnik
|
|
58
|
+
|
|
59
|
+
args:
|
|
60
|
+
copy: Copy the RTEMS binaries to the IOCs TFTP and NFS directories first
|
|
61
|
+
reboot: Reboot the IOC once the binaries are copied and the connection is made
|
|
62
|
+
"""
|
|
63
|
+
print(
|
|
64
|
+
f"Remote control startup of RTEMS IOC {GLOBALS.IOC_NAME}"
|
|
65
|
+
f" at {GLOBALS.RTEMS_IOC_IP}"
|
|
66
|
+
)
|
|
67
|
+
if copy:
|
|
68
|
+
copy_rtems()
|
|
69
|
+
ioc_connect(GLOBALS.RTEMS_CONSOLE, reboot=reboot)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@cli.command()
|
|
73
|
+
def dev(
|
|
74
|
+
ioc_repo: Path = typer.Argument(
|
|
75
|
+
...,
|
|
76
|
+
help="The beamline/accelerator repo holding the IOC instance",
|
|
77
|
+
file_okay=False,
|
|
78
|
+
exists=True,
|
|
79
|
+
),
|
|
80
|
+
ioc_name: str = typer.Argument(
|
|
81
|
+
...,
|
|
82
|
+
help="The name of the IOC instance to work on",
|
|
83
|
+
),
|
|
84
|
+
):
|
|
85
|
+
"""
|
|
86
|
+
Sets up a devcontainer to work on an IOC instance. Must be run from within
|
|
87
|
+
the developer container for the generic IOC that the instance uses.
|
|
88
|
+
|
|
89
|
+
args:
|
|
90
|
+
ioc_repo: The path to the IOC repository that holds the instance
|
|
91
|
+
ioc_name: The name of the IOC instance to work on
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
ioc_path = ioc_repo / "services" / ioc_name
|
|
95
|
+
|
|
96
|
+
values = ioc_repo / "helm/shared/values.yaml"
|
|
97
|
+
if not values.exists():
|
|
98
|
+
typer.echo(f"Global settings file {values} not found. Exiting")
|
|
99
|
+
raise typer.Exit(1)
|
|
100
|
+
|
|
101
|
+
ioc_values = ioc_path / "values.yaml"
|
|
102
|
+
if not ioc_values.exists():
|
|
103
|
+
typer.echo(f"Instance settings file {ioc_values} not found. Exiting")
|
|
104
|
+
raise typer.Exit(1)
|
|
105
|
+
|
|
106
|
+
env_vars = {}
|
|
107
|
+
# TODO in future use pydantic and make a model for this but for now let's cheese it.
|
|
108
|
+
with open(values) as fp:
|
|
109
|
+
yaml = YAML(typ="safe").load(fp)
|
|
110
|
+
try:
|
|
111
|
+
ioc_group = yaml["ioc-instance"]["ioc_group"]
|
|
112
|
+
for item in yaml["ioc-instance"]["globalEnv"]:
|
|
113
|
+
env_vars[item["name"]] = item["value"]
|
|
114
|
+
except KeyError:
|
|
115
|
+
typer.echo(f"{values} not in expected format")
|
|
116
|
+
raise typer.Exit(1) from None
|
|
117
|
+
|
|
118
|
+
with open(ioc_values) as fp:
|
|
119
|
+
yaml = YAML(typ="safe").load(fp)
|
|
120
|
+
try:
|
|
121
|
+
for item in yaml["shared"]["ioc-instance"]["iocEnv"]:
|
|
122
|
+
env_vars[item["name"]] = item["value"]
|
|
123
|
+
except KeyError:
|
|
124
|
+
typer.echo(f"{ioc_values} not in expected format")
|
|
125
|
+
raise typer.Exit(1) from None
|
|
126
|
+
|
|
127
|
+
this_dir = Path(__file__).parent
|
|
128
|
+
template = Path(this_dir / "rsync.sh.jinja").read_text()
|
|
129
|
+
|
|
130
|
+
script = Template(template).render(
|
|
131
|
+
env_vars=env_vars,
|
|
132
|
+
ioc_group=ioc_group,
|
|
133
|
+
ioc_name=ioc_name,
|
|
134
|
+
ioc_path=ioc_path,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
script_file = Path("/tmp/dev_proxy.sh")
|
|
138
|
+
script_file.write_text(script)
|
|
139
|
+
|
|
140
|
+
typer.echo(f"\nIOC {ioc_name} dev environment prepared for {ioc_repo}")
|
|
141
|
+
typer.echo("You can now change and compile support module or iocs.")
|
|
142
|
+
typer.echo("Then start the ioc with '/epics/ioc/start.sh'")
|
|
143
|
+
typer.echo(f"\n\nPlease first source {script_file} to set up the dev environment.")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# test with:
|
|
147
|
+
# pipenv run python -m ibek
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
cli()
|
|
@@ -14,24 +14,30 @@ def copy_rtems():
|
|
|
14
14
|
Copy RTEMS binaries to a location where the RTEMS IOC can access them
|
|
15
15
|
"""
|
|
16
16
|
# root of pvc mount into which we copy the IOC files for the RTEMS IOC to access
|
|
17
|
-
root = GLOBALS.
|
|
17
|
+
root = GLOBALS.RTEMS_TFTP_PATH
|
|
18
18
|
# root of the path that the RTEMS IOC expects to find the IOC files
|
|
19
|
-
|
|
19
|
+
RTEMS_TFTP_PATH = Path("/iocs") / GLOBALS.IOC_GROUP / GLOBALS.IOC_NAME
|
|
20
20
|
# where to copy the Generic IOC folder to (at present only holds the dbd folder)
|
|
21
|
-
|
|
21
|
+
ioc_dest = root / "ioc"
|
|
22
|
+
# where to copy the generated runtime assets to (st.cmd and ioc.db)
|
|
22
23
|
dest_runtime = root / "runtime"
|
|
23
24
|
|
|
24
|
-
# because we are moving the ioc files we need to fix up startup script paths
|
|
25
|
-
startup = GLOBALS.RUNTIME / "st.cmd"
|
|
26
|
-
cmd_txt = startup.read_text()
|
|
27
|
-
cmd_txt = re.sub("/epics/", f"{str(rtems_root)}/", cmd_txt)
|
|
28
|
-
startup.write_text(cmd_txt)
|
|
29
|
-
|
|
30
|
-
# clean up previous IOC binaries
|
|
31
|
-
shutil.rmtree(dest_ioc, ignore_errors=True)
|
|
32
|
-
shutil.rmtree(dest_runtime, ignore_errors=True)
|
|
33
25
|
# move all the files needed for runtime into the PVC that is being shared
|
|
34
26
|
# over nfs/tftp by the nfsv2-tftp service
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
ioc_src = GLOBALS.IOC.readlink()
|
|
28
|
+
dbd_src = ioc_src / "dbd"
|
|
29
|
+
dbd_dest = ioc_dest / "dbd"
|
|
30
|
+
binary = Path("bin/RTEMS-beatnik/ioc.boot")
|
|
31
|
+
bin_rtems_src = ioc_src / binary
|
|
32
|
+
bin_rtems_dest = ioc_dest / binary
|
|
33
|
+
bin_rtems_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
shutil.copytree(dbd_src, dbd_dest, symlinks=True, dirs_exist_ok=True)
|
|
36
|
+
shutil.copy(bin_rtems_src, bin_rtems_dest)
|
|
37
37
|
shutil.copytree(GLOBALS.RUNTIME, dest_runtime, dirs_exist_ok=True)
|
|
38
|
+
|
|
39
|
+
# because we moved the ioc files we need to fix up startup script paths
|
|
40
|
+
startup = dest_runtime / "st.cmd"
|
|
41
|
+
cmd_txt = startup.read_text()
|
|
42
|
+
cmd_txt = re.sub("/epics/", f"{str(RTEMS_TFTP_PATH)}/", cmd_txt)
|
|
43
|
+
startup.write_text(cmd_txt)
|
|
@@ -32,7 +32,7 @@ class _Globals:
|
|
|
32
32
|
# TODO in future, shall we drop the RTEMS prefix and make this module
|
|
33
33
|
# generic?
|
|
34
34
|
|
|
35
|
-
self.
|
|
35
|
+
self.RTEMS_TFTP_PATH = Path(os.getenv("RTEMS_TFTP_PATH", "/nfsv2-tftp"))
|
|
36
36
|
""" root folder of a mounted PVC in which to place IOC binaries """
|
|
37
37
|
|
|
38
38
|
self.RTEMS_IOC_IP = os.getenv("RTEMS_IOC_IP")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
{% for name,value in env_vars.items() -%}
|
|
4
|
+
export {{ name }}={{ value }}
|
|
5
|
+
{% endfor -%}
|
|
6
|
+
export IOC_GROUP={{ ioc_group }}
|
|
7
|
+
export IOC_NAME={{ ioc_name }}
|
|
8
|
+
export IOC_PATH={{ ioc_path }}
|
|
9
|
+
|
|
10
|
+
pkill -f rsync-background &>/dev/null
|
|
11
|
+
|
|
12
|
+
ibek dev instance $IOC_PATH
|
|
13
|
+
|
|
14
|
+
mkdir -p $RTEMS_TFTP_PATH
|
|
15
|
+
cd $RTEMS_TFTP_PATH
|
|
16
|
+
|
|
17
|
+
# get previous contents
|
|
18
|
+
rsync -rt "rsync://$RTEMS_TFTP_IP:12002/files/$IOC_GROUP/$IOC_NAME/" $RTEMS_TFTP_PATH 2>/dev/null
|
|
19
|
+
|
|
20
|
+
echo "
|
|
21
|
+
#!/bin/bash
|
|
22
|
+
|
|
23
|
+
while true; do
|
|
24
|
+
inotifywait -e modify,create,delete,move -r $RTEMS_TFTP_PATH
|
|
25
|
+
rsync -rt --delete /$RTEMS_TFTP_PATH/ \
|
|
26
|
+
"rsync://$RTEMS_TFTP_IP:12002/files/$IOC_GROUP/$IOC_NAME/" &> /tmp/rsync.log
|
|
27
|
+
done
|
|
28
|
+
" > /tmp/rsync-background.sh
|
|
29
|
+
|
|
30
|
+
nohup bash /tmp/rsync-background.sh &> /tmp/rsync-background.log &
|
|
31
|
+
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import signal
|
|
2
|
+
import sys
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from time import sleep
|
|
5
|
+
|
|
6
|
+
import pexpect
|
|
7
|
+
|
|
8
|
+
from .utils import run_command
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CannotConnect(Exception):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RtemsState(Enum):
|
|
16
|
+
MOT = 0
|
|
17
|
+
IOC = 2
|
|
18
|
+
UNKNOWN = 3
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TelnetRTEMS:
|
|
22
|
+
"""
|
|
23
|
+
A class for connecting to an RTEMS MVME5500 IOC over telnet.
|
|
24
|
+
|
|
25
|
+
properties:
|
|
26
|
+
_hostname: the hostname of the terminal server connected to the IOC
|
|
27
|
+
_port: the port of the terminal server connected to the IOC
|
|
28
|
+
_ioc_reboot: a flag to determine if the IOC should be rebooted
|
|
29
|
+
_child: the pexpect child object for the initial telnet session
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
MOT_PROMPT = "MVME5500> $"
|
|
33
|
+
CONTINUE = "<SPC> to Continue"
|
|
34
|
+
REBOOTED = "TCP Statistics"
|
|
35
|
+
IOC_STARTED = "iocRun: All initialization complete"
|
|
36
|
+
IOC_CHECK = "\ntaskwdShow"
|
|
37
|
+
IOC_RESPONSE = "free nodes"
|
|
38
|
+
NO_CONNECTION = "Connection closed by foreign host"
|
|
39
|
+
|
|
40
|
+
def __init__(self, host_and_port: str, ioc_reboot: bool):
|
|
41
|
+
self._hostname, self._port = host_and_port.split(":")
|
|
42
|
+
self._ioc_reboot = ioc_reboot
|
|
43
|
+
self._child = None
|
|
44
|
+
|
|
45
|
+
self.ioc_rebooted = False
|
|
46
|
+
self.command = f"telnet {self._hostname} {self._port}"
|
|
47
|
+
|
|
48
|
+
signal.signal(signal.SIGINT, self.terminate)
|
|
49
|
+
signal.signal(signal.SIGTERM, self.terminate)
|
|
50
|
+
|
|
51
|
+
def report(self, message):
|
|
52
|
+
"""
|
|
53
|
+
print a message that is noticeable amongst all the other output
|
|
54
|
+
"""
|
|
55
|
+
print(f"\n>>>> {message} <<<<\n")
|
|
56
|
+
|
|
57
|
+
def terminate(self, signum, frame):
|
|
58
|
+
"""
|
|
59
|
+
Allow the user to terminate the connection with ctrl-c while the
|
|
60
|
+
pexpect child is running (but not once interactive telnet is started)
|
|
61
|
+
"""
|
|
62
|
+
self.report("Terminating")
|
|
63
|
+
exit(0)
|
|
64
|
+
|
|
65
|
+
def connect(self):
|
|
66
|
+
"""
|
|
67
|
+
connect to an IOC over telnet using pexpect and determine if we are
|
|
68
|
+
at the bootloader or IOC shell. If we are at the bootloader, we will
|
|
69
|
+
reboot the IOC into the IOC shell, we will also reboot if the ioc_reboot
|
|
70
|
+
flag was set in the constructor.
|
|
71
|
+
"""
|
|
72
|
+
self._child = pexpect.spawn(
|
|
73
|
+
self.command,
|
|
74
|
+
encoding="utf-8",
|
|
75
|
+
logfile=sys.stdout,
|
|
76
|
+
echo=False,
|
|
77
|
+
codec_errors="ignore",
|
|
78
|
+
)
|
|
79
|
+
try:
|
|
80
|
+
# first check for connection refusal
|
|
81
|
+
self._child.expect(self.NO_CONNECTION, timeout=1)
|
|
82
|
+
except pexpect.exceptions.TIMEOUT:
|
|
83
|
+
# if we timeout looking for failed connection that is good
|
|
84
|
+
pass
|
|
85
|
+
else:
|
|
86
|
+
print(">> Cannot connect to remote IOC, connection in use? <<")
|
|
87
|
+
raise CannotConnect
|
|
88
|
+
|
|
89
|
+
def check_prompt(self, retries=5) -> RtemsState:
|
|
90
|
+
"""
|
|
91
|
+
Determine if we are currently seeing an IOC shell prompt or
|
|
92
|
+
bootloader. Because there is a possibility that we are in the middle
|
|
93
|
+
of a reboot, we will retry for one before giving up.
|
|
94
|
+
"""
|
|
95
|
+
while retries > 0:
|
|
96
|
+
try:
|
|
97
|
+
# see if we are in the IOC shell
|
|
98
|
+
self._child.sendline(self.IOC_CHECK)
|
|
99
|
+
self._child.expect(self.IOC_RESPONSE, timeout=1)
|
|
100
|
+
except pexpect.exceptions.TIMEOUT:
|
|
101
|
+
try:
|
|
102
|
+
# see if we are in the bootloader
|
|
103
|
+
self._child.sendline()
|
|
104
|
+
self._child.expect(self.MOT_PROMPT, timeout=1)
|
|
105
|
+
except pexpect.exceptions.TIMEOUT:
|
|
106
|
+
# current state unknown. wait and retry
|
|
107
|
+
sleep(15)
|
|
108
|
+
else:
|
|
109
|
+
self.report("Currently in bootloader")
|
|
110
|
+
return RtemsState.MOT
|
|
111
|
+
else:
|
|
112
|
+
self.report("Currently in IOC shell")
|
|
113
|
+
return RtemsState.IOC
|
|
114
|
+
|
|
115
|
+
self.report("Retrying get current status")
|
|
116
|
+
retries -= 1
|
|
117
|
+
|
|
118
|
+
self.report("Current state UNKNOWN")
|
|
119
|
+
raise CannotConnect("Current state of remote IOC unknown")
|
|
120
|
+
|
|
121
|
+
def reboot(self, into: RtemsState):
|
|
122
|
+
"""
|
|
123
|
+
Reboot the board from IOC shell or bootloader and choose appropriate
|
|
124
|
+
options to get to the state requested by the into argument.
|
|
125
|
+
"""
|
|
126
|
+
self.report(f"Rebooting into {into.name}")
|
|
127
|
+
current_state = self.check_prompt()
|
|
128
|
+
if current_state == RtemsState.MOT:
|
|
129
|
+
self._child.sendline("reset")
|
|
130
|
+
else:
|
|
131
|
+
self._child.sendline("exit")
|
|
132
|
+
|
|
133
|
+
self._child.expect(self.CONTINUE, timeout=10)
|
|
134
|
+
if into == RtemsState.MOT:
|
|
135
|
+
# send escape to get into the bootloader
|
|
136
|
+
self._child.sendline(chr(27))
|
|
137
|
+
else:
|
|
138
|
+
# send space to boot the IOC
|
|
139
|
+
self._child.send(" ")
|
|
140
|
+
|
|
141
|
+
def get_epics_prompt(self):
|
|
142
|
+
"""
|
|
143
|
+
Get to the IOC shell prompt, if the IOC is not already running, reboot
|
|
144
|
+
it into the IOC shell. If the IOC is running, do a reboot only if
|
|
145
|
+
requested (in order to pick up new binaries/startup/epics db)
|
|
146
|
+
"""
|
|
147
|
+
current = self.check_prompt()
|
|
148
|
+
if current != RtemsState.IOC:
|
|
149
|
+
sleep(0.2)
|
|
150
|
+
self._child.reboot(RtemsState.IOC)
|
|
151
|
+
self.ioc_rebooted = True
|
|
152
|
+
self._child.expect(self.IOC_STARTED, timeout=50)
|
|
153
|
+
else:
|
|
154
|
+
if self._ioc_reboot and not self.ioc_rebooted:
|
|
155
|
+
self.ioc_rebooted = True
|
|
156
|
+
self.reboot(RtemsState.IOC)
|
|
157
|
+
self._child.expect(self.IOC_STARTED, timeout=50)
|
|
158
|
+
|
|
159
|
+
self.report("press enter for IOC shell prompt")
|
|
160
|
+
|
|
161
|
+
def get_boot_prompt(self):
|
|
162
|
+
"""
|
|
163
|
+
Get to the bootloader prompt, if the IOC shell is running then exit
|
|
164
|
+
and send appropriate commands to get to the bootloader
|
|
165
|
+
"""
|
|
166
|
+
current = self.check_prompt()
|
|
167
|
+
if current != RtemsState.MOT:
|
|
168
|
+
# get out of the IOC and return to MOT
|
|
169
|
+
self.reboot(RtemsState.MOT)
|
|
170
|
+
self._child.expect(self.MOT_PROMPT, timeout=20)
|
|
171
|
+
|
|
172
|
+
self.report("press enter for bootloader prompt")
|
|
173
|
+
|
|
174
|
+
def close(self):
|
|
175
|
+
if self._child:
|
|
176
|
+
self._child.close()
|
|
177
|
+
self._child = None
|
|
178
|
+
|
|
179
|
+
def __del__(self):
|
|
180
|
+
self.close()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def ioc_connect(host_and_port: str, reboot: bool = False):
|
|
184
|
+
"""
|
|
185
|
+
Entrypoint to make a connection to an RTEMS IOC over telnet.
|
|
186
|
+
Once connected, enters an interactive user session with the IOC.
|
|
187
|
+
|
|
188
|
+
args:
|
|
189
|
+
host_and_port: 'hostname:port' of the IOC to connect to
|
|
190
|
+
reboot: reboot the IOC to pick up new binaries/startup/epics db
|
|
191
|
+
"""
|
|
192
|
+
telnet = TelnetRTEMS(host_and_port, reboot)
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
telnet.connect()
|
|
196
|
+
telnet.get_epics_prompt()
|
|
197
|
+
except (CannotConnect, pexpect.exceptions.TIMEOUT):
|
|
198
|
+
print("\n\nNot Connected. Exiting...")
|
|
199
|
+
telnet.close()
|
|
200
|
+
else:
|
|
201
|
+
telnet.close()
|
|
202
|
+
run_command(telnet.command)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def run_command(
|
|
8
|
+
command: str, interactive=True, error_OK=False, show=False
|
|
9
|
+
) -> Union[str, bool]:
|
|
10
|
+
"""
|
|
11
|
+
Run a command and return the output
|
|
12
|
+
|
|
13
|
+
if interactive is true then allow stdin and stdout, return the return code,
|
|
14
|
+
otherwise return True for success and False for failure
|
|
15
|
+
|
|
16
|
+
args:
|
|
17
|
+
|
|
18
|
+
command: the command to run
|
|
19
|
+
interactive: if True then allow stdin and stdout
|
|
20
|
+
error_OK: if True then do not raise an exception on failure
|
|
21
|
+
show: typer.echo the command output to the console
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
p_result = subprocess.run(command, capture_output=not interactive, shell=True)
|
|
25
|
+
|
|
26
|
+
if interactive:
|
|
27
|
+
output = error_out = ""
|
|
28
|
+
else:
|
|
29
|
+
output = p_result.stdout.decode()
|
|
30
|
+
error_out = p_result.stderr.decode()
|
|
31
|
+
|
|
32
|
+
if interactive:
|
|
33
|
+
result: Union[str, bool] = p_result.returncode == 0
|
|
34
|
+
else:
|
|
35
|
+
result = output + error_out
|
|
36
|
+
|
|
37
|
+
if p_result.returncode != 0 and not error_OK:
|
|
38
|
+
typer.echo("\nCommand Failed:")
|
|
39
|
+
if not globals.EC_VERBOSE:
|
|
40
|
+
typer.echo(command)
|
|
41
|
+
typer.echo(output)
|
|
42
|
+
typer.echo(error_out)
|
|
43
|
+
raise typer.Exit(1)
|
|
44
|
+
|
|
45
|
+
if show:
|
|
46
|
+
typer.echo(output)
|
|
47
|
+
typer.echo(error_out)
|
|
48
|
+
|
|
49
|
+
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: rtems-proxy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Support for a K8S proxy container in controlling and monitoring RTEMS EPICS IOCs
|
|
5
5
|
Author-email: Giles Knap <giles.knap@diamond.ac.uk>
|
|
6
6
|
License: Apache License
|
|
@@ -216,8 +216,11 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
216
216
|
Requires-Python: >=3.7
|
|
217
217
|
Description-Content-Type: text/markdown
|
|
218
218
|
License-File: LICENSE
|
|
219
|
-
Requires-Dist:
|
|
219
|
+
Requires-Dist: jinja2
|
|
220
|
+
Requires-Dist: pexpect
|
|
221
|
+
Requires-Dist: ruamel.yaml
|
|
220
222
|
Requires-Dist: telnetlib3
|
|
223
|
+
Requires-Dist: typer
|
|
221
224
|
Provides-Extra: dev
|
|
222
225
|
Requires-Dist: copier; extra == "dev"
|
|
223
226
|
Requires-Dist: mypy; extra == "dev"
|
|
@@ -30,7 +30,9 @@ src/rtems_proxy/__main__.py
|
|
|
30
30
|
src/rtems_proxy/_version.py
|
|
31
31
|
src/rtems_proxy/copy.py
|
|
32
32
|
src/rtems_proxy/globals.py
|
|
33
|
+
src/rtems_proxy/rsync.sh.jinja
|
|
33
34
|
src/rtems_proxy/telnet.py
|
|
35
|
+
src/rtems_proxy/utils.py
|
|
34
36
|
src/rtems_proxy.egg-info/PKG-INFO
|
|
35
37
|
src/rtems_proxy.egg-info/SOURCES.txt
|
|
36
38
|
src/rtems_proxy.egg-info/dependency_links.txt
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
from time import sleep
|
|
2
|
-
from typing import Optional
|
|
3
|
-
|
|
4
|
-
import typer
|
|
5
|
-
|
|
6
|
-
from . import __version__
|
|
7
|
-
from .copy import copy_rtems
|
|
8
|
-
from .globals import GLOBALS
|
|
9
|
-
from .telnet import connect
|
|
10
|
-
|
|
11
|
-
__all__ = ["main"]
|
|
12
|
-
|
|
13
|
-
cli = typer.Typer()
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def version_callback(value: bool):
|
|
17
|
-
if value:
|
|
18
|
-
typer.echo(__version__)
|
|
19
|
-
raise typer.Exit()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@cli.callback()
|
|
23
|
-
def main(
|
|
24
|
-
version: Optional[bool] = typer.Option(
|
|
25
|
-
None,
|
|
26
|
-
"--version",
|
|
27
|
-
callback=version_callback,
|
|
28
|
-
is_eager=True,
|
|
29
|
-
help="Print the version of ibek and exit",
|
|
30
|
-
),
|
|
31
|
-
):
|
|
32
|
-
"""
|
|
33
|
-
Proxy for RTEMS IOCs controlling and monitoring
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@cli.command()
|
|
38
|
-
def start(
|
|
39
|
-
copy: bool = typer.Option(
|
|
40
|
-
True, "--copy/--no-copy", help="copy binaries before connecting"
|
|
41
|
-
),
|
|
42
|
-
reboot: bool = typer.Option(
|
|
43
|
-
True, "--reboot/--no-reboot", help="reboot the IOC first"
|
|
44
|
-
),
|
|
45
|
-
):
|
|
46
|
-
"""
|
|
47
|
-
Start the RTEMS IOC
|
|
48
|
-
"""
|
|
49
|
-
print(
|
|
50
|
-
f"Remote control startup of RTEMS IOC {GLOBALS.IOC_NAME}"
|
|
51
|
-
f" at {GLOBALS.RTEMS_IOC_IP}"
|
|
52
|
-
)
|
|
53
|
-
if copy:
|
|
54
|
-
copy_rtems()
|
|
55
|
-
connect(GLOBALS.RTEMS_CONSOLE, reboot=reboot)
|
|
56
|
-
|
|
57
|
-
while True:
|
|
58
|
-
print(f"\n\nIOC {GLOBALS.IOC_NAME} disconnected. Reconnect or exit? [r/e]")
|
|
59
|
-
choice = ""
|
|
60
|
-
while choice not in ["r", "e"]:
|
|
61
|
-
choice = input()
|
|
62
|
-
if choice == "e":
|
|
63
|
-
break
|
|
64
|
-
connect(GLOBALS.RTEMS_CONSOLE)
|
|
65
|
-
sleep(10)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# test with:
|
|
69
|
-
# pipenv run python -m ibek
|
|
70
|
-
if __name__ == "__main__":
|
|
71
|
-
cli()
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import signal
|
|
3
|
-
import sys
|
|
4
|
-
import termios
|
|
5
|
-
import tty
|
|
6
|
-
from time import sleep
|
|
7
|
-
|
|
8
|
-
import telnetlib3
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class TelnetRTEMS:
|
|
12
|
-
def __init__(self, hostname: str, port: int, reboot: bool, pause: bool):
|
|
13
|
-
self.hostname = hostname
|
|
14
|
-
self.port = port
|
|
15
|
-
self.reboot = reboot
|
|
16
|
-
self.pause = pause
|
|
17
|
-
self.running = True
|
|
18
|
-
self.terminated = False
|
|
19
|
-
signal.signal(signal.SIGINT, self.terminate)
|
|
20
|
-
signal.signal(signal.SIGTERM, self.terminate)
|
|
21
|
-
|
|
22
|
-
def terminate(self, *args):
|
|
23
|
-
self.running = False
|
|
24
|
-
self.terminated = True
|
|
25
|
-
|
|
26
|
-
async def user_input(self, writer):
|
|
27
|
-
def get_char():
|
|
28
|
-
ch = sys.stdin.read(1)
|
|
29
|
-
return ch
|
|
30
|
-
|
|
31
|
-
stdin_fd = sys.stdin.fileno()
|
|
32
|
-
old_settings = termios.tcgetattr(stdin_fd)
|
|
33
|
-
|
|
34
|
-
try:
|
|
35
|
-
tty.setraw(sys.stdin.fileno())
|
|
36
|
-
loop = asyncio.events._get_running_loop()
|
|
37
|
-
|
|
38
|
-
while self.running:
|
|
39
|
-
# run the wait for input in a separate thread
|
|
40
|
-
next_ch = await loop.run_in_executor(None, get_char)
|
|
41
|
-
# look for control + ] to terminate the session
|
|
42
|
-
if b"\x1d" in next_ch.encode():
|
|
43
|
-
self.running = False
|
|
44
|
-
break
|
|
45
|
-
writer.write(next_ch)
|
|
46
|
-
|
|
47
|
-
finally:
|
|
48
|
-
termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings)
|
|
49
|
-
writer.close()
|
|
50
|
-
|
|
51
|
-
async def server_output(self, reader):
|
|
52
|
-
while self.running:
|
|
53
|
-
out_p = await reader.read(1024)
|
|
54
|
-
if not out_p:
|
|
55
|
-
raise EOFError("Connection closed by server")
|
|
56
|
-
print(out_p, flush=True, end="")
|
|
57
|
-
reader.close()
|
|
58
|
-
|
|
59
|
-
async def shell(self, reader, writer):
|
|
60
|
-
# user input and server output in separate tasks
|
|
61
|
-
tasks = [
|
|
62
|
-
self.server_output(reader),
|
|
63
|
-
self.user_input(writer),
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
await asyncio.gather(*tasks)
|
|
67
|
-
|
|
68
|
-
async def send_command(self, cmd):
|
|
69
|
-
reader, writer = await telnetlib3.open_connection(self.hostname, self.port)
|
|
70
|
-
|
|
71
|
-
writer.write("\r")
|
|
72
|
-
await asyncio.sleep(0.1)
|
|
73
|
-
prompt = await reader.read(1024)
|
|
74
|
-
print(f"prompt is {prompt.strip()}")
|
|
75
|
-
|
|
76
|
-
print(f"Sending command: {cmd}")
|
|
77
|
-
writer.write(f"{cmd}\r")
|
|
78
|
-
await asyncio.sleep(0.1)
|
|
79
|
-
result = await reader.read(1024)
|
|
80
|
-
print(f"Result is: {result.strip()}")
|
|
81
|
-
|
|
82
|
-
reader.close()
|
|
83
|
-
writer.close()
|
|
84
|
-
|
|
85
|
-
async def connect(self):
|
|
86
|
-
while True: # retry loop
|
|
87
|
-
try:
|
|
88
|
-
if self.reboot:
|
|
89
|
-
print("REBOOTING IOC ...")
|
|
90
|
-
await self.send_command("exit")
|
|
91
|
-
self.reboot = False # only reboot once
|
|
92
|
-
elif self.pause:
|
|
93
|
-
print("Un-stopping IOC")
|
|
94
|
-
await self.send_command("iocRun")
|
|
95
|
-
|
|
96
|
-
# start interactive session
|
|
97
|
-
reader, writer = await telnetlib3.open_connection(
|
|
98
|
-
self.hostname, self.port, shell=self.shell
|
|
99
|
-
)
|
|
100
|
-
await writer.protocol.waiter_closed
|
|
101
|
-
|
|
102
|
-
if self.terminated and self.pause:
|
|
103
|
-
print("Stopping IOC")
|
|
104
|
-
await self.send_command("iocPause")
|
|
105
|
-
|
|
106
|
-
break # interactive session done so exit retry loop
|
|
107
|
-
|
|
108
|
-
except ConnectionResetError:
|
|
109
|
-
# probably the previous pod is terminating and is still connected
|
|
110
|
-
print("Waiting for Telnet Port (connection reset), RETRYING ...")
|
|
111
|
-
sleep(3)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def connect(host_and_port: str, reboot: bool = False, pause: bool = False):
|
|
115
|
-
hostname, port = host_and_port.split(":")
|
|
116
|
-
telnet = TelnetRTEMS(hostname, int(port), reboot, pause)
|
|
117
|
-
asyncio.run(telnet.connect())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|