From fadb811f74c482fdf794318835d1bc80f65d430a Mon Sep 17 00:00:00 2001 From: giles knap Date: Wed, 4 Dec 2024 15:50:59 +0000 Subject: [PATCH] add explanation of CA PVA in containers --- .devcontainer/postCreateCommand | 2 +- ...acces_tests.sh => channel_access_tests.sh} | 46 ++++++++----- docs/demo/pv_access_tests.sh | 69 +++++++++++++++++++ docs/explanations/epics_protocols.md | 66 ++++++++++++++++++ docs/tutorials/dev_container.md | 26 +++---- docs/tutorials/setup_workstation.md | 34 ++++++++- 6 files changed, 203 insertions(+), 40 deletions(-) rename docs/demo/{channel_acces_tests.sh => channel_access_tests.sh} (71%) create mode 100755 docs/demo/pv_access_tests.sh create mode 100644 docs/explanations/epics_protocols.md diff --git a/.devcontainer/postCreateCommand b/.devcontainer/postCreateCommand index 710308a1..ec8e173d 100644 --- a/.devcontainer/postCreateCommand +++ b/.devcontainer/postCreateCommand @@ -18,7 +18,7 @@ fi ################################################################################ # pick a theme that does not cause completion corruption in zsh -sed -i $HOME/.zshrc -e 's/ZSH_THEME="devcontainers"/ZSH_THEME="lukerandall"/' +sed -i $HOME/.zshrc -re 's/^ZSH_THEME=.*/ZSH_THEME="dst"/' # allow personalization of all devcontainers in this subdirectory # by placing a .devcontainer_rc file in the workspace root diff --git a/docs/demo/channel_acces_tests.sh b/docs/demo/channel_access_tests.sh similarity index 71% rename from docs/demo/channel_acces_tests.sh rename to docs/demo/channel_access_tests.sh index 381081c1..5a844700 100755 --- a/docs/demo/channel_acces_tests.sh +++ b/docs/demo/channel_access_tests.sh @@ -1,20 +1,29 @@ #!/bin/bash -# demo of exposing channel access outside of a container +# demo of exposing Channel Access outside of a container -cmd='-dit --rm --name test ghcr.io/epics-containers/ioc-adsimdetector-demo:2024.11.1' +# caRepeater: +# +# note that these experiments ignore the CA_REPEATER_PORT. Typically +# IOCs in containers should also expose 5065 for the CA repeater. +# Because only the first IOC needs to start caRepeater, and that one process +# binds to 5065, it turns out that caRepeater continues to work as expected. +# (caRepeater can go down if the IOC that started it goes down, but it will get +# restarted by the next IOC startup.) + +cmd='-dit --rm --name test ghcr.io/epics-containers/ioc-template-example-runtime:4.1.0' check () { podman run $args $env $ports $cmd > /dev/null podman logs -f test | grep -q -m 1 "iocInit" - if [[ $(caget BL01T-EA-TST-02:DET:Acquire 2>/dev/null) =~ "Acquire" ]]; then + if caget EXAMPLE:IBEK:SUM &>/dev/null; then echo "CA Success" else echo "CA Failure" fi - podman stop test &> /dev/null + podman stop test &> /dev/null; sleep 1 echo --- } @@ -22,34 +31,35 @@ check () { echo no ports, network host, broadcast ports= args="--network host" - check #success + check # the default sledgehammer approach works like native IOCs ) +# I guess broadcasts don't go to the loopback ( - echo 5064, broadcast + echo 5064, broadcast: FAILURE ports="-p 5064:5064 -p 5064:5064/udp" - check #success + check ) ( echo 5064 no UDP, broadcast: FAILURE ports="-p 5064:5064" - check #failure + check ) ( echo 5064, unicast export EPICS_CA_ADDR_LIST="localhost" ports="-p 5064:5064 -p 5064:5064/udp" - check #success + check ) ( echo 5064 no UDP, unicast: FAILURE export EPICS_CA_ADDR_LIST="localhost" ports="-p 5064:5064" - check #failure + check # EPICS_CA_ADDR_LIST uses UDP Unicast ) @@ -59,7 +69,7 @@ check () { ( echo 5064, broadcast, localhost: FAILURE ports="-p 127.0.0.1:5064:5064 -p 127.0.0.1:5064:5064/udp" - check #failure + check # why does this fail? - I guess broadcasts do not go to localhost ) @@ -67,7 +77,7 @@ check () { echo 5064, unicast, localhost export EPICS_CA_ADDR_LIST="localhost" ports="-p 127.0.0.1:5064:5064 -p 127.0.0.1:5064:5064/udp" - check #success + check ) ( @@ -75,7 +85,7 @@ check () { export EPICS_CA_SERVER_PORT=8064 env="-e EPICS_CA_SERVER_PORT=8064" ports="-p 8064:8064 -p 8064:8064/udp" - check #success + check ) ( @@ -83,7 +93,7 @@ check () { export EPICS_CA_ADDR_LIST="localhost" EPICS_CA_SERVER_PORT=8064 env="-e EPICS_CA_SERVER_PORT=8064" ports="-p 127.0.0.1:8064:8064 -p 127.0.0.1:8064:8064/udp" - check #success + check ) # remapping the ports does not work! @@ -91,26 +101,26 @@ check () { echo 8064:5064, broadcast: FAILURE export EPICS_CA_SERVER_PORT=8064 ports="-p 8064:5064 -p 8064:5064/udp" - check #failure + check ) ( echo 8064:5064, unicast, localhost: FAILURE export EPICS_CA_ADDR_LIST="localhost" EPICS_CA_SERVER_PORT=8064 ports="-p 127.0.0.1:8064:5064 -p 127.0.0.1:8064:5064/udp" - check #failure + check ) ( echo 5064 no UDP, NAME_SERVER, localhost export EPICS_CA_NAME_SERVERS="localhost:5064" ports="-p 127.0.0.1:5064:5064" - check #success + check ) ( echo 8064:5064 no UDP, NAME_SERVER, localhost export EPICS_CA_NAME_SERVERS="localhost:8064" ports="-p 127.0.0.1:8064:5064" - check #success + check ) diff --git a/docs/demo/pv_access_tests.sh b/docs/demo/pv_access_tests.sh new file mode 100755 index 00000000..f55496e8 --- /dev/null +++ b/docs/demo/pv_access_tests.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# demo of exposing PV Access outside of a container + +# requires a venv with p4p installed + +pvget=' +from p4p.client.thread import Context + +Context("pva").get("EXAMPLE:IBEK:SUM", timeout=0.5) +' + +cmd='-dit --rm --name test ghcr.io/epics-containers/ioc-template-example-runtime:4.1.0' + +check () { + podman run $args $env $ports $cmd > /dev/null + podman logs -f test | grep -q -m 1 "iocInit" + + if python -c "$pvget" 2>/dev/null; then + echo "PVA Success" + else + echo "PVA Failure" + fi + + podman stop test &> /dev/null + echo --- +} + +( + echo no ports, network host, broadcast + ports= + args="--network host" + check + # the default sledgehammer approach works like native IOCs +) + +# PVA fails for broadcast and unicast because the client creates a new random +# port for the server to make the TCP circuit but that is not NAT friendly. +( + echo 5075, broadcast: FAILURE + ports="-p 5075:5075 -p 5075:5075/udp" + check +) + +( + echo 5075, unicast: FAILURE + export EPICS_PVA_ADDR_LIST="localhost" + ports="-p 5075:5075 -p 5075:5075/udp" + check +) + +# NAME SERVER uses a single TCP connection and is compatible with NAT +# +# IMPORTANT - for this to work, both ends of the conversation must be pvxs. +# Thus to talk to ADPvaPlugin requires a pvagw running in the same container +# network to proxy the traffic +( + echo 5075, NAME SERVER + export EPICS_PVA_NAME_SERVERS="localhost:5075" + ports="-p 5075:5075" + check +) + +( + echo 8057:5075, NAME SERVER + export EPICS_PVA_NAME_SERVERS="localhost:8075" + ports="-p 8075:5075" + check +) diff --git a/docs/explanations/epics_protocols.md b/docs/explanations/epics_protocols.md new file mode 100644 index 00000000..2156e6ab --- /dev/null +++ b/docs/explanations/epics_protocols.md @@ -0,0 +1,66 @@ +# EPICS Network Protocols in Containers + +When EPICS IOCs run in containers, Channel Access or PVAcess protocols must be made available to clients. There are some challenges around this that are discussed in this page. + + +## Approaches to Network Protocols + +To get clients and servers connected we can use 3 approaches: + +1. Run IOC containers in **Host Network**: + - This is the approach that DLS has adopted for IOCs running in Kubernetes. + - The container uses the host network stack. + - This looks identical to running the IOC on the host machine as far as clients are concerned. + - See a discussion of the reasoning here: [](./net_protocols.md) + - This reduces the isolation of the container from the host so additional security measures may be needed. +2. Use **Port Mapping**: + - This approach is used in the developer containers defined by [ioc-template](https://github.com/epics-containers/example-services) + - The container runs in a container network. + - The necessary ports are mapped from the host network to the container network. + - VSCode can do this port mapping automatically when it detects processes binding to ports. + - This approach is good for local development and running tutorials as the mapping can be made to localhost only and PVs can be isolated to the developer's machine. +3. Run the clients in the **same container network** as the IOCs: + - This approach is used in [example-services](https://github.com/epics-containers/example-services). + - **example-services** runs a PVA and a CA gateway in the same container network as the IOCs. + - The gateways use Port Mapping to give access to their own clients. + - The gateways can use any ports and UDP broadcast to communicate with the IOCs. + - If your client is a GUI app, like phoebus, then this may not work as it can be difficult to do X11 forwarding into a rootless container network. + +## General Observations + +Using Host Network or the same container network for client and host is compatible with both PVA and CA protocols. + +For podman and docker networks this is true even for UDP broadcast. + +For the majority of Kubernetes CNI's the broadcast does not work across pods. It is quite possible that broadcast within pods would work as this is equivalent to 'same container network'. However this would make management of large numbers of IOCs far more of a manual task. + + +## Channel Access + +Specification . + +Experiments with Channel Access servers running in containers reveal: +- Port Mapping works for CA including UDP broadcast. +- But UDP broadcast only works if the container does not remap the port to a different number inside the container. +- Using CA Name Server works for Port Mapping + + +## PV Access + +Specification . + +Experimentation with PV Access servers running in containers reveal: +- Port Mapping works for PVA always fails because PVA servers open a new random port for each circuit and this is not NAT friendly. +- Using EPICS_PVA_NAME_SERVERS works for Port Mapping. +- But the client and server must both be PVXS +- To talk to a non PVXS server, a pvagw running in the same container network may be used. + +## Code + +The following bash scripts can be run to test the assertions made above: + +```{literalinclude} ../demo/channel_access_tests.sh +``` + +```{literalinclude} ../demo/pv_access_tests.sh +``` diff --git a/docs/tutorials/dev_container.md b/docs/tutorials/dev_container.md index 49ad0e10..00096d50 100644 --- a/docs/tutorials/dev_container.md +++ b/docs/tutorials/dev_container.md @@ -102,24 +102,6 @@ for details. ## Starting a Developer Container -:::{Warning} -DLS Users and Redhat Users: - -There is a -[bug in VSCode devcontainers extension](https://github.com/microsoft/vscode-remote-release/issues/8557) -at the time of writing that makes it incompatible with docker and an SELinux -enabled /tmp directory. This will affect most Redhat users and you will see an -error regarding permissions on the /tmp folder when VSCode is building your -devcontainer. - -Here is a workaround that disables SELinux labels in podman. -Paste this into a terminal: - -```bash -sed -i ~/.config/containers/containers.conf -e '/label=false/d' -e '/^\[containers\]$/a label=false' -``` -::: - ### Preparation For this section we will work with the ADSimDetector Generic IOC that we used in previous tutorials. Let's go and fetch a version of the Generic IOC source and build it locally. @@ -132,8 +114,16 @@ cd t01-services . ./environment.sh docker compose down ``` + +One issue with compose is that you must have the project available to use compose commands. If you have removed t01-services then you can stop all the services manually instead. To stop and remove all containers on your workstation use the following command: + +```bash +docker stop $(docker ps -q) +``` ::: + + For the purposes of this tutorial we will place the source in a folder right next to your test beamline `t01-services` folder: diff --git a/docs/tutorials/setup_workstation.md b/docs/tutorials/setup_workstation.md index 48aafde7..ba95db7b 100644 --- a/docs/tutorials/setup_workstation.md +++ b/docs/tutorials/setup_workstation.md @@ -141,11 +141,39 @@ The docker install page encourages you to install Docker Desktop. This is a paid ### Docker Compose For Podman Users -docker compose allows you to define and run multi-container Docker applications. epics-containers uses it for describing a set of IOCs and other services that are deployed together. +docker compose allows you to define and run multi-container Docker applications. epics-containers uses it for describing a set of IOCs and other services that are deployed together. It is a useful starting point for tutorials before moving on to Kubernetes. It could also form the basis of a production deployment for those not using Kubernetes. -If you installed docker using the above instructions then docker compose is already installed. If you installed podman then you will need to install docker compose separately. We prefer to use docker-compose instead of podman-compose because it is more widely used and avoids behaviour differences between the two tools. If you are at DLS you just need to run 'module load docker-compose' to get access to docker compose with podman as the back end. +If you installed docker using the above instructions then docker compose is already installed. If you installed podman then you will need to install docker compose separately. We prefer to use docker-compose instead of podman-compose because it is more widely used and there are still some issues with podman-compose at the time of writing. -Other users of podman please see these instructions [rootless podman with docker-compose](https://www.redhat.com/sysadmin/podman-docker-compose). You need only read the section titled "Start the Podman system service" (the rest of the page validates the setup). +:::{Note} +**DLS Users**: docker compose integration with podman is available on RHEL 8 Workstations at DLS. Run `module load docker-compose` to enable it. +::: + +Steps to combine podman and docker-compose:- + +1. Launch a podman user service and expose a docker API socket as follows. This step need only be done once per workstation. + + ```bash + systemctl enable --user podman.socket --now + ``` +1. Add the following to your shell profile (e.g. ~/.bashrc or ~/.zshrc) to instruct docker-compose and any other docker tool to use podman's docker API socket. + + ```bash + export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock + ``` + +1. Use these instructions to install the docker compose binary. Some linux distributions have docker-compose in their package manager, this is the easiest way to install it if available. + +1. we recommend uninstalling podman-compose if you have it installed. + + ```bash + # Debian/Ubuntu + sudo apt uninstall podman-compose + # RHEL/Centos + sudo dnf remove podman-compose + # Arch + sudo pacman -R podman-compose + ``` ### Important Notes Regarding docker and podman