Skip to content

Commit

Permalink
add reading of environment and rsync to the tftp server
Browse files Browse the repository at this point in the history
  • Loading branch information
gilesknap committed Mar 22, 2024
1 parent e38befa commit 8c17c29
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 26 deletions.
14 changes: 7 additions & 7 deletions proxy-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@
set -x

# This is the folder the PVC for the nfsv2tftp shared volume is mounted into.
export RTEMS_ROOT=${RTEMS_ROOT:-/nfsv2-tftp}
export RTEMS_TFTP_PATH=${RTEMS_TFTP_PATH:-/nfsv2-tftp}

if [ ! -d ${RTEMS_ROOT} ]; then
if [ ! -d ${RTEMS_TFTP_PATH} ]; then
echo "ERROR: No PVC folder found."
# make a folder for testing outside of the cluster
mkdir -p ${RTEMS_ROOT}
mkdir -p ${RTEMS_TFTP_PATH}
fi

# copy the IOC instance's runtime assets into the shared volume
cp -rL /epics/ioc ${RTEMS_ROOT}
cp -r /epics/runtime ${RTEMS_ROOT}
cp -rL /epics/ioc ${RTEMS_TFTP_PATH}
cp -r /epics/runtime ${RTEMS_TFTP_PATH}
# move binary to the root for shorter paths
mv ${RTEMS_ROOT}/ioc/bin/*/ioc.boot ${RTEMS_ROOT}
mv ${RTEMS_TFTP_PATH}/ioc/bin/*/ioc.boot ${RTEMS_TFTP_PATH}
# fix up the paths in st.cmd
sed -i "s|/epics/|/iocs/${IOC_LOCATION}/${IOC_NAME}/|" ${RTEMS_ROOT}/runtime/st.cmd
sed -i "s|/epics/|/iocs/${IOC_LOCATION}/${IOC_NAME}/|" ${RTEMS_TFTP_PATH}/runtime/st.cmd

# keep the container running ...
while true; do
Expand Down
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
]
description = "Support for a K8S proxy container in controlling and monitoring RTEMS EPICS IOCs"
dependencies = ["typer", "telnetlib3"]
dependencies = ["typer", "jinja2", "ruamel.yaml", "telnetlib3"]
dynamic = ["version"]
license.file = "LICENSE"
readme = "README.md"
Expand Down Expand Up @@ -89,6 +89,11 @@ commands =
"""

[tool.ruff]
ignore = [
"B008", # Do not perform unnecessary work in __all__
"C408", # Unnecessary collection call - e.g. list(...) instead of [...]
"E501", # Line too long, should be fixed by black.
]
src = ["src", "tests"]
line-length = 88
lint.select = [
Expand Down
90 changes: 89 additions & 1 deletion src/rtems_proxy/__main__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from pathlib import Path
from time import sleep
from typing import Optional

import typer
from jinja2 import Template
from ruamel.yaml import YAML

from . import __version__
from .copy import copy_rtems
Expand Down Expand Up @@ -44,7 +47,18 @@ def start(
),
):
"""
Start the RTEMS IOC
Starts an RTEMS IOC. Places the IOC binaries in the expected location,
restarts the IOC and connects stdio to the IOC console.
This should be called inside of a runtime IOC container after ibek
has generated the runtime assets for the IOC.
The standard 'start.sh' in the runtime IOC will call this entry point if
it detects that EPICS_HOST_ARCH==RTEMS-beatnik
args:
copy: Copy the RTEMS binaries to the IOCs TFTP and NFS directories first
reboot: Reboot the IOC once the binaries are copied and the connection is made
"""
print(
f"Remote control startup of RTEMS IOC {GLOBALS.IOC_NAME}"
Expand All @@ -65,6 +79,80 @@ def start(
sleep(10)


@cli.command()
def dev(
ioc_repo: Path = typer.Argument(
...,
help="The beamline/accelerator repo holding the IOC instance",
file_okay=False,
exists=True,
),
ioc_name: str = typer.Argument(
...,
help="The name of the IOC instance to work on",
),
):
"""
Sets up a devcontainer to work on an IOC instance. Must be run from within
the developer container for the generic IOC that the instance uses.
args:
ioc_repo: The path to the IOC repository that holds the instance
ioc_name: The name of the IOC instance to work on
"""

ioc_path = ioc_repo / "services" / ioc_name

values = ioc_repo / "helm/shared/values.yaml"
if not values.exists():
typer.echo(f"Global settings file {values} not found. Exiting")
raise typer.Exit(1)

ioc_values = ioc_path / "values.yaml"
if not ioc_values.exists():
typer.echo(f"Instance settings file {ioc_values} not found. Exiting")
raise typer.Exit(1)

env_vars = {}
# TODO in future use pydantic and make a model for this but for now let's cheese it.
with open(values) as fp:
yaml = YAML(typ="safe").load(fp)
try:
ioc_group = yaml["ioc-instance"]["ioc_group"]
for item in yaml["ioc-instance"]["globalEnv"]:
env_vars[item["name"]] = item["value"]
except KeyError:
typer.echo(f"{values} not in expected format")
raise typer.Exit(1) from None

with open(ioc_values) as fp:
yaml = YAML(typ="safe").load(fp)
try:
for item in yaml["shared"]["ioc-instance"]["iocEnv"]:
env_vars[item["name"]] = item["value"]
except KeyError:
typer.echo(f"{ioc_values} not in expected format")
raise typer.Exit(1) from None

this_dir = Path(__file__).parent
template = Path(this_dir / "rsync.sh.jinja").read_text()

script = Template(template).render(
env_vars=env_vars,
ioc_group=ioc_group,
ioc_name=ioc_name,
ioc_path=ioc_path,
)

script_file = Path("/tmp/dev_proxy.sh")
script_file.write_text(script)

typer.echo(f"\nIOC {ioc_name} dev environment prepared for {ioc_repo}")
typer.echo("You can now change and compile support module or iocs.")
typer.echo("Then start the ioc with '/epics/ioc/start.sh'")
typer.echo(f"\n\nPlease first source {script_file} to set up the dev environment.")


# test with:
# pipenv run python -m ibek
if __name__ == "__main__":
Expand Down
25 changes: 15 additions & 10 deletions src/rtems_proxy/copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import re
import shutil
from pathlib import Path
from time import sleep

from .globals import GLOBALS

Expand All @@ -14,26 +15,30 @@ def copy_rtems():
Copy RTEMS binaries to a location where the RTEMS IOC can access them
"""
# root of pvc mount into which we copy the IOC files for the RTEMS IOC to access
root = GLOBALS.RTEMS_ROOT
root = GLOBALS.RTEMS_TFTP_PATH
# root of the path that the RTEMS IOC expects to find the IOC files
rtems_root = Path("/iocs") / GLOBALS.IOC_GROUP / GLOBALS.IOC_NAME
RTEMS_TFTP_PATH = Path("/iocs") / GLOBALS.IOC_GROUP / GLOBALS.IOC_NAME
# where to copy the Generic IOC folder to (at present only holds the dbd folder)
dest_ioc = root / "ioc"
ioc_dest = root / "ioc"
# where to copy the generated runtime assets to (st.cmd and ioc.db)
dest_runtime = root / "runtime"

# clean up previous IOC binaries
shutil.rmtree(dest_ioc, ignore_errors=True)
shutil.rmtree(dest_runtime, ignore_errors=True)

# move all the files needed for runtime into the PVC that is being shared
# over nfs/tftp by the nfsv2-tftp service
Path.mkdir(dest_ioc, exist_ok=True)
shutil.copytree(GLOBALS.IOC / "dbd", dest_ioc, symlinks=True, dirs_exist_ok=True)
ioc_src = GLOBALS.IOC.readlink()
dbd_src = ioc_src / "dbd"
dbd_dest = ioc_dest / "dbd"
binary = Path("bin/RTEMS-beatnik/ioc.boot")
bin_rtems_src = ioc_src / binary
bin_rtems_dest = ioc_dest / binary
bin_rtems_dest.parent.mkdir(parents=True, exist_ok=True)

shutil.copytree(dbd_src , dbd_dest, symlinks=True, dirs_exist_ok=True)
shutil.copy(bin_rtems_src, bin_rtems_dest)
shutil.copytree(GLOBALS.RUNTIME, dest_runtime, dirs_exist_ok=True)

# because we moved the ioc files we need to fix up startup script paths
startup = dest_runtime / "st.cmd"
cmd_txt = startup.read_text()
cmd_txt = re.sub("/epics/", f"{str(rtems_root)}/", cmd_txt)
cmd_txt = re.sub("/epics/", f"{str(RTEMS_TFTP_PATH)}/", cmd_txt)
startup.write_text(cmd_txt)
2 changes: 1 addition & 1 deletion src/rtems_proxy/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __init__(self) -> None:
# TODO in future, shall we drop the RTEMS prefix and make this module
# generic?

self.RTEMS_ROOT = Path(os.getenv("RTEMS_ROOT", "/nfsv2-tftp"))
self.RTEMS_TFTP_PATH = Path(os.getenv("RTEMS_TFTP_PATH", "/nfsv2-tftp"))
""" root folder of a mounted PVC in which to place IOC binaries """

self.RTEMS_IOC_IP = os.getenv("RTEMS_IOC_IP")
Expand Down
31 changes: 31 additions & 0 deletions src/rtems_proxy/rsync.sh.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/bash

{% for name,value in env_vars.items() -%}
export {{ name }}={{ value }}
{% endfor -%}
export IOC_GROUP={{ ioc_group }}
export IOC_NAME={{ ioc_name }}
export IOC_PATH={{ ioc_path }}

pkill -f rsync-background &>/dev/null

ibek dev instance $IOC_PATH

mkdir -p $RTEMS_TFTP_PATH
cd $RTEMS_TFTP_PATH

# get previous contents
rsync -rt "rsync://$RTEMS_TFTP_IP:12002/files/$IOC_GROUP/$IOC_NAME/" $RTEMS_TFTP_PATH 2>/dev/null

echo "
#!/bin/bash
while true; do
inotifywait -e modify,create,delete,move -r $RTEMS_TFTP_PATH
rsync -rt --delete /$RTEMS_TFTP_PATH/ \
"rsync://$RTEMS_TFTP_IP:12002/files/$IOC_GROUP/$IOC_NAME/" &> /tmp/rsync.log
done
" > /tmp/rsync-background.sh

nohup bash /tmp/rsync-background.sh &> /tmp/rsync-background.log &

14 changes: 8 additions & 6 deletions src/rtems_proxy/telnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ def get_char():
writer.close()

async def server_output(self, reader):
while self.running:
out_p = await reader.read(1024)
if not out_p:
raise EOFError("Connection closed by server")
print(out_p, flush=True, end="")
reader.close()
try:
while self.running:
out_p = await reader.read(1024)
if not out_p:
self.running = False
print(out_p, flush=True, end="")
finally:
reader.close()

async def shell(self, reader, writer):
# user input and server output in separate tasks
Expand Down
49 changes: 49 additions & 0 deletions src/rtems_proxy/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import subprocess
from typing import Union

import typer


def run_command(
command: str, interactive=True, error_OK=False, show=False
) -> Union[str, bool]:
"""
Run a command and return the output
if interactive is true then allow stdin and stdout, return the return code,
otherwise return True for success and False for failure
args:
command: the command to run
interactive: if True then allow stdin and stdout
error_OK: if True then do not raise an exception on failure
show: typer.echo the command output to the console
"""

p_result = subprocess.run(command, capture_output=not interactive, shell=True)

if interactive:
output = error_out = ""
else:
output = p_result.stdout.decode()
error_out = p_result.stderr.decode()

if interactive:
result: Union[str, bool] = p_result.returncode == 0
else:
result = output + error_out

if p_result.returncode != 0 and not error_OK:
typer.echo("\nCommand Failed:")
if not globals.EC_VERBOSE:
typer.echo(command)
typer.echo(output)
typer.echo(error_out)
raise typer.Exit(1)

if show:
typer.echo(output)
typer.echo(error_out)

return result

0 comments on commit 8c17c29

Please sign in to comment.