Skip to content

Commit

Permalink
add explanation of CA PVA in containers
Browse files Browse the repository at this point in the history
  • Loading branch information
gilesknap committed Dec 4, 2024
1 parent 8983aad commit fadb811
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 40 deletions.
2 changes: 1 addition & 1 deletion .devcontainer/postCreateCommand
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,65 @@
#!/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 ---
}

(
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
)

Expand All @@ -59,58 +69,58 @@ 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
)

(
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
)

(
echo 8064, broadcast
export EPICS_CA_SERVER_PORT=8064
env="-e EPICS_CA_SERVER_PORT=8064"
ports="-p 8064:8064 -p 8064:8064/udp"
check #success
check
)

(
echo 8064, unicast, localhost
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!
(
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
)
69 changes: 69 additions & 0 deletions docs/demo/pv_access_tests.sh
Original file line number Diff line number Diff line change
@@ -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
)
66 changes: 66 additions & 0 deletions docs/explanations/epics_protocols.md
Original file line number Diff line number Diff line change
@@ -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 <https://docs.epics-controls.org/en/latest/internal/ca_protocol.html>.

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 <https://docs.epics-controls.org/en/latest/pv-access/Protocol-Messages.html>.

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
```
26 changes: 8 additions & 18 deletions docs/tutorials/dev_container.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:

Expand Down
34 changes: 31 additions & 3 deletions docs/tutorials/setup_workstation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://docs.docker.com/compose/install/standalone> 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
Expand Down

0 comments on commit fadb811

Please sign in to comment.