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.
Files changed (47) hide show
  1. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/PKG-INFO +5 -2
  2. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/proxy-start.sh +7 -7
  3. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/pyproject.toml +6 -1
  4. rtems-proxy-0.3.0/src/rtems_proxy/__main__.py +149 -0
  5. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy/_version.py +2 -2
  6. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy/copy.py +20 -14
  7. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy/globals.py +1 -1
  8. rtems-proxy-0.3.0/src/rtems_proxy/rsync.sh.jinja +31 -0
  9. rtems-proxy-0.3.0/src/rtems_proxy/telnet.py +202 -0
  10. rtems-proxy-0.3.0/src/rtems_proxy/utils.py +49 -0
  11. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy.egg-info/PKG-INFO +5 -2
  12. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy.egg-info/SOURCES.txt +2 -0
  13. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy.egg-info/requires.txt +4 -1
  14. rtems-proxy-0.2.4/src/rtems_proxy/__main__.py +0 -71
  15. rtems-proxy-0.2.4/src/rtems_proxy/telnet.py +0 -117
  16. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.copier-answers.yml +0 -0
  17. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.devcontainer/devcontainer.json +0 -0
  18. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/CONTRIBUTING.md +0 -0
  19. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/actions/install_requirements/action.yml +0 -0
  20. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/dependabot.yml +0 -0
  21. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/pages/index.html +0 -0
  22. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/pages/make_switcher.py +0 -0
  23. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/workflows/_check.yml +0 -0
  24. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/workflows/_dist.yml +0 -0
  25. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/workflows/_pypi.yml +0 -0
  26. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/workflows/_release.yml +0 -0
  27. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/workflows/_test.yml +0 -0
  28. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/workflows/_tox.yml +0 -0
  29. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.github/workflows/ci.yml +0 -0
  30. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.gitignore +0 -0
  31. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.pre-commit-config.yaml +0 -0
  32. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.vscode/extensions.json +0 -0
  33. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.vscode/launch.json +0 -0
  34. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.vscode/settings.json +0 -0
  35. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/.vscode/tasks.json +0 -0
  36. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/Dockerfile +0 -0
  37. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/LICENSE +0 -0
  38. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/README.md +0 -0
  39. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/catalog-info.yaml +0 -0
  40. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/requirements.txt +0 -0
  41. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/setup.cfg +0 -0
  42. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy/__init__.py +0 -0
  43. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy.egg-info/dependency_links.txt +0 -0
  44. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy.egg-info/entry_points.txt +0 -0
  45. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/src/rtems_proxy.egg-info/top_level.txt +0 -0
  46. {rtems-proxy-0.2.4 → rtems-proxy-0.3.0}/tests/conftest.py +0 -0
  47. {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.2.4
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: typer
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 RTEMS_ROOT=${RTEMS_ROOT:-/nfsv2-tftp}
6
+ export RTEMS_TFTP_PATH=${RTEMS_TFTP_PATH:-/nfsv2-tftp}
7
7
 
8
- if [ ! -d ${RTEMS_ROOT} ]; then
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 ${RTEMS_ROOT}
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 ${RTEMS_ROOT}
16
- cp -r /epics/runtime ${RTEMS_ROOT}
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 ${RTEMS_ROOT}/ioc/bin/*/ioc.boot ${RTEMS_ROOT}
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}/|" ${RTEMS_ROOT}/runtime/st.cmd
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 = ["typer", "telnetlib3"]
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()
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.2.4'
16
- __version_tuple__ = version_tuple = (0, 2, 4)
15
+ __version__ = version = '0.3.0'
16
+ __version_tuple__ = version_tuple = (0, 3, 0)
@@ -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.RTEMS_ROOT
17
+ root = GLOBALS.RTEMS_TFTP_PATH
18
18
  # root of the path that the RTEMS IOC expects to find the IOC files
19
- rtems_root = Path("/iocs") / GLOBALS.IOC_GROUP / GLOBALS.IOC_NAME
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
- dest_ioc = root / "ioc"
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
- Path.mkdir(dest_ioc, exist_ok=True)
36
- shutil.copytree(GLOBALS.IOC / "dbd", dest_ioc, symlinks=True, dirs_exist_ok=True)
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.RTEMS_ROOT = Path(os.getenv("RTEMS_ROOT", "/nfsv2-tftp"))
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.2.4
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: typer
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,5 +1,8 @@
1
- typer
1
+ jinja2
2
+ pexpect
3
+ ruamel.yaml
2
4
  telnetlib3
5
+ typer
3
6
 
4
7
  [dev]
5
8
  copier
@@ -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