For the integration of existing developments into docker/kubernates we use part of the  Diamond flow for a beamline under kubernates.

In particular we have realised our customisation where we put all the normalisable support we have for our INFN's beamlines.

The project is INFN EPICS IOC by cloning it:

cloning the project
git clone https://baltig.infn.it/epics-containers/infn-epics-ioc.git --recurse-submodules


The aim of this project:

  1. create a docker image with the device support needed for INFN beamlines that can be run in container.
  2. standardize and greatly  simplify the way to instantiate iocs via simple yaml file
  3. have a common database (shared among INFN and Diamond)
  4. standardize the build system

The layout of this project is:

Project layout
.
├── Dockerfile   				<-- docker file production
├── LICENSE
├── README.md
├── build
├── ibek-support				<-- submodule with ibek device support
│   ├── ADAravis
│   ├── ADCore
│   ├── ADGenICam
│   ├── ADSimDetector
│   ├── IOCInfo
│   ├── LICENSE
│   ├── README.md
│   ├── StreamDevice
│   ├── _global
│   ├── asyn
│   ├── autosave
│   ├── build_support.sh
│   ├── busy
│   ├── calc
│   ├── easy-driver-epics
│   ├── iocStats
│   ├── lakeshore340
│   ├── make_global_schemas.sh
│   ├── modbus
│   ├── motor
│   ├── motorMotorSim
│   ├── motorNewport
│   ├── opcua
│   ├── pmac
│   ├── pvi-generate.sh
│   ├── quadEM
│   ├── schemas
│   ├── screen-epics-ioc
│   ├── sequencer
│   ├── sscan
│   ├── tests
│   ├── todo
│   └── zebra
├── ioc
│   ├── Makefile
│   ├── configure
│   ├── install.sh
│   ├── iocApp
│   ├── liveness.sh
│   ├── start.sh
│   └── stop.sh
├── menlo
│   ├── install.sh
│   └── menlo.ibek.support.yaml
├── requirements.txt
├── requirements_ec.txt
├── tests
│   ├── example-config
│   ├── example-ibek-config
│   └── run-tests.sh
└── user_examples
    └── README.md



When we have to put an IOC in production we may have the following possibility:

  1. support already present in <ibek-support>
  2. support present as git project
  3. support not present at all → put in the condition 2

Use an existing ibek support

If a support is present in <ibek-support>, it's very simple to use it just create a yaml that instantiates  your device(s).

An example could be the following the following yaml newport.yaml. It instatiate a motor with 1 axis. This support implicitly assumes that this serial control is connected to a Ethernet2Serial server like a moxa that responds on address   192.168.190.56 and port 4001


newport.yaml
ioc_name: rfmotors
description: RF motors 

entities:

  - type: motorNewport.SMC100CreateController
    controllerName: NEWPORT001
    P: "SPARC:RF:"
    IP: 192.168.190.56
    TCPPORT: 4001
    numAxes: 1

  
  - type: motorNewport.motorAxis
    controller: NEWPORT001
    M: "m0"
    DESC: "Axis"
    ADDR: 0
    DLLM: -25
    DHLM: 25
    home: 1
    start: 10
    VELO: 1


Then we can test if simply having the docker image: baltig.infn.it:4567/epics-containers/infn-epics-ioc


Docker run as ioc
docker run -p 5064:5064/udp -p 5064:5064/tcp -p 5065:5065/udp -p 5065:5065/tcp -it -v .:/epics/ioc/config baltig.infn.it:4567/epics-containers/infn-epics-ioc:devel 


A generic asyn motor OPI interface  can be used to drive the motor. Motor OPIs Remember to add localhost to your phoebus 

org.phoebus.pv.ca/addr_list settings:

Phoebus settings example
org.phoebus.pv.ca/addr_list=localhost
org.phoebus.pv.ca/auto_addr_list=false
org.csstudio.trends.databrowser3/urls=pbraw://sparc-archiver.apps.okd-datest.lnf.infn.it/retrieval
org.csstudio.trends.databrowser3/archives=pbraw://sparc-archiver.apps.okd-datest.lnf.infn.it/retrieval
org.phoebus.olog.es.api/olog_url=http://sparc-olog.apps.okd-datest.lnf.infn.it/Olog
org.phoebus.olog.api/olog_url=http://sparc-olog.apps.okd-datest.lnf.infn.it/Olog
org.phoebus.logbook/logbook_factory=olog-es
org.phoebus.olog.api/username=epics
org.phoebus.olog.api/password=epics
org.phoebus.channelfinder/channelfinder.serviceURL=http://sparc-channelfinder.apps.okd-datest.lnf.infn.it/ChannelFinder
org.phoebus.applications.saveandrestore.client/jmasar.service.url=http://sparc-saveandrestore.apps.okd-datest.lnf.infn.it/save-restore
org.csstudio.scan.client/host=http://sparc-scanserver.apps.okd-datest.lnf.infn.it
org.csstudio.scan.client/port=4810


Once the IOC runs correctly as docker can be inserted in the EPIK8S configuration


Build a support from existing git support

This is path is more complex, but gives much more satisfaction, in fact each time you add a new support as much generic possible you greatly simplify the life of your self and in principle many other people will use the your support.

Step 1 create a directory inside ibek-support


Create by copy and renaming one of the existing support. Suppose you want to add the support for <mydevmodule>, you should have:

Project layout
.
├── Dockerfile
├── LICENSE
├── README.md
├── build
├── ibek-support
│   ├── ADAravis
│   ├── mydevmodule
│   │   ├── install.sh
│   │   └── mydevmodule.ibek.support.yaml
│   ├── opcua
│   │   └── install.sh
│   ├── pmac
│   │   ├── install.sh
│   │   ├── make_pvi.sh
│   │   ├── pmac.ibek.support.todo
│   │   ├── pmac.ibek.support.yaml
│   │   ├── pmacAxis.pvi.device.yaml
│   │   ├── pmacCSController.pvi.device.yaml
│   │   ├── pmacController.pvi.device.yaml
│   │   └── pmacTrajectory.pvi.device.yaml
│   



So in general just two file must be added: install.sh and mydevmodule.ibek.support.yaml.

install.sh

This file  contains instructions on howto retrieve, compile and link  dependencies of your module mydevmodule. In general if the mydevmodule  is well structured from a build epics point of view needs only to be patched like that:

install.sh
#!/bin/bash

# ARGUMENTS:
#  $1 VERSION to install (must match repo tag)
VERSION=${1}
NAME=<mydevmodule>  ## as compare in the git repository
FOLDER=$(dirname $(readlink -f $0))

# log output and abort on failure
set -xe

# get the source and fix up the configure/RELEASE files
ibek support git-clone ${NAME} ${VERSION} --org http://gitrepo ## repository git root
ibek support register ${NAME}

# declare the libs and DBDs that are required in ioc/iocApp/src/Makefile
ibek support add-libs <mydevmodulelib> <support1lib> <support2lib> asyn ## optional libraries that must be linked
ibek support add-dbds <mydevmodule>.dbd <optsupport1.dbd> <optsupport2>.dbd ### optional dbd supports
# global config settings
${FOLDER}/../_global/install.sh ${NAME}

# compile the support module
ibek support compile ${NAME}
# prepare *.bob, *.pvi, *.ibek.support.yaml for access outside the container.
ibek support generate-links ${FOLDER}




mydevmodule.ibek.support.yaml

This also has a very simple syntax and gives instructions on how to expand a future yaml configuration.

The simplest way to proceed is to understand how the st.cmd of your ioc should be constructed and having this in mind build a set of rules that wraps that code.

The best way is look other supports to understand, here below I put a recently support that I added for newport motors. The comments at the beginning is more a general st.cmd of a SMC100 newport motor.

In the example yaml I instruct ibek to correcty replace a file configuration like the previous newport.yaml.


Example
# yaml-language-server: $schema=https://github.com/epics-containers/ibek/releases/download/1.5.0/ibek.support.schema.json

# #errlogInit(5000)
# < envPaths
# # Tell EPICS all about the record types, device-support modules, drivers,
# # etc.
# dbLoadDatabase("../../dbd/newport.dbd")
# newport_registerRecordDeviceDriver(pdbbase)

# ### Motors
# dbLoadTemplate "motor.substitutions.SMC100"

# ### Serial port setup
# drvAsynSerialPortConfigure("serial1", "/dev/ttyS0", 0, 0, 0)
# asynSetOption(serial1,0,baud,57600)
# asynOctetSetInputEos("serial1",0,"\r\n")
# asynOctetSetOutputEos("serial1",0,"\r\n")

# ### Newport SMC100 support
# # (driver port, serial port, axis num, ms mov poll, ms idle poll, egu per step)
# SMC100CreateController("SMC100_1", "serial1",1, 100, 0, "0.00005")

# file "$(TOP)/db/basic_asyn_motor.db"
# {
# pattern
# {P,      N,     M,         DTYP,      PORT,  ADDR,    DESC,        EGU,     DIR,  VELO,  VBAS,  ACCL,  BDST,  BVEL,  BACC,  MRES,  PREC,  DHLM,  DLLM,  INIT, RTRY}
# {IOC:,  1,  "m$(N)",  "asynMotor",  "SMC100_1",  0,  "GTS30V",      mm,     Pos,  1,     0,    .2,    0,     .5,     .2,    0.00001,  6,     25,   -5,  ""}
# }
# iocInit

module: motorNewport

defs:
  - name: SMC100CreateController
    description: |-
      Creates a SMC100 motion controller connected to an ethernetToSerialServer

    args:
      - type: id
        name: controllerName
        description: |-
          The name of the controller and its Asyn Port Name
      
      - type: str
        name: P
        description: |-
          Device PV Prefix

      - type: str
        name: IP
        description: |-
          IP address of the ethernet2serial
        default: 127.0.0.1 ## localhost

      - type: int
        name: TCPPORT
        description: |-
          Port of the ethernet2serial
        default: 4001
      
      - type: int
        name: POLL
        description: |-
          Movement poll ms
        default: 100
      
      - type: float
        name: EGUXSTEP
        description: |-
          EGU PER STEP
        default: 0.00005

      - type: int
        name: ASYNPRIO
        description: |-
          ASYN   PRIORITY, Default : 0
        default: 0

      - type: int
        name: AUTOCONNECT
        description: |-
          Asyn auto connect
          0: Auto connection
          1: no Auto connection
        default: 0

      - type: int
        name: NOPRECESSESOS
        description: |-
          ASYN   noProcessEos, Default : 0
          https://epics.anl.gov/tech-talk/2020/msg01705.php
        default: 0

      

      - type: int
        name: numAxes
        description: |-
          The number of axes to create

    pre_init:

      - value: |
          # epicsEnvSet "STREAM_PROTOCOL_PATH", "$(MOTORNEWPORT)/protocol/"
          # Create Asyn Port
          drvAsynIPPortConfigure("{{controllerName}}_ASYN", "{{IP}}:{{TCPPORT}}", {{ASYNPRIO}}, {{AUTOCONNECT}}, {{NOPRECESSESOS}})
          # asynInterposeEosConfig("{{controllerName}}_ASYN",0,2000,0)
          SMC100CreateController("SMC100_{{controllerName}}", "{{controllerName}}_ASYN","{{numAxes}}", "{{POLL}}", 0, "{{EGUXSTEP}}")
          asynOctetSetInputEos({{controllerName}}_ASYN,0,"\r\n")
          asynOctetSetOutputEos({{controllerName}}_ASYN,0,"\r\n")
          asynReport 10
          

  - name: motorAxis
    description: |-
      Creates a motor axis

    args:
      - type: object
        name: controller
        description: |-
          a reference to the motion controller

      - type: str
        name: M
        description: |-
          PV suffix for the motor record

      - type: int
        name: ADDR
        description: |-
          The axis number (allowed to be from 0 to controller.numAxes-1)

      - type: str
        name: DESC
        description: |-
          The description of the axis

      - type: int
        name: DLLM
        description: |-
          The low limit of the axis
        default: -5

      - type: int
        name: DHLM
        description: |-
          The high limit of the axis
        default: 25

      - type: int
        name: VELO
        description: |-
          Velocity
        default: 1

      - type: int
        name: home
        description: |-
          The home position of the axis (in counts)

      - type: int
        name: start
        description: |-
          The starting position of the axis (in counts)
        default: 0

      - type: enum
        name: DIR
        description: |-
          The direction of the axis
        default: 0
        values:
          Pos: 0
          Neg: 1

      - type: str
        name: EGU
        description: |-
          Engineering Units
        default: "mm"
      
      - type: float
        name: VBAS
        description: |-
          Base Velocity (EGU/s)
        default: 0.2

      - type: float
        name: ACCL
        description: |-
          Seconds to Velocity
        default: 0.2

      - type: int
        name: BDST
        description: |-
          BL Distance (EGU)
        default: 0

      - type: float
        name: BVEL
        description: |-
          BL Velocity (EGU/s)
        default: 0.5

      - type: float
        name: BACC
        description: |-
          BL Seconds to Veloc.
        default: 0.2

      - type: float
        name: MRES
        description: |-
          Motor Step Size (EGU)
        default: 0.00001

      - type: int
        name: PREC
        description: |-
          Display precision (EGU)
        default: 6
      
    databases:
      # TODO as this is a simulation I have hard coded some of the DB fields,
      # but these could easily be made into arguments above
      #
      # Note: supplying no value means that the argument of the same name is used
      # (the most common case - if you contrive to make args and db fields the same.
      # Which is  good idea for ease of transition from traditional IOCs)
      - file: basic_asyn_motor.db
        args:
          P: "{{controller.P}}"
          N: "{{ADDR +1 }}"
          M:
          DTYP: "asynMotor"
          PORT: "SMC100_{{controller}}"
          ADDR:
          DESC:
          EGU: 
          DIR:
          VELO:
          VBAS:
          ACCL:
          BDST:
          BVEL:
          BACC:
          MRES: 
          PREC:
          DHLM: 
          DLLM: 
          INIT: ""
    post_init:
      - value: |
          dbl


Step 2 add an entry into Dockerfile

Modify the Dockerfile in the project:

Dockerfile
##### build stage ##############################################################

ARG TARGET_ARCHITECTURE
ARG BASE=7.0.8ec1b3
ARG REGISTRY=ghcr.io/epics-containers

FROM  ${REGISTRY}/epics-base-${TARGET_ARCHITECTURE}-developer:${BASE} AS developer


# The devcontainer mounts the project root to /epics/generic-source
# Using the same location here makes devcontainer/runtime differences transparent.
ENV SOURCE_FOLDER=/epics/generic-source
# connect ioc source folder to its know location
RUN ln -s ${SOURCE_FOLDER}/ioc ${IOC}

# Get latest ibek while in development. Will come from epics-base when stable
COPY requirements.txt requirements.txt
RUN pip install --upgrade -r requirements.txt

WORKDIR ${SOURCE_FOLDER}/ibek-support

# copy the global ibek files
COPY ibek-support/_global/ _global

COPY ibek-support/iocStats/ iocStats
RUN iocStats/install.sh 3.2.0

################################################################################
#  TODO - Add further support module installations here
################################################################################

COPY ibek-support/asyn/ asyn/
RUN asyn/install.sh R4-44

COPY ibek-support/autosave/ autosave/
RUN autosave/install.sh R5-11

COPY ibek-support/busy/ busy/
RUN busy/install.sh R1-7-3

COPY ibek-support/StreamDevice/ StreamDevice/
RUN StreamDevice/install.sh 2.8.24

COPY ibek-support/sscan/ sscan/
RUN sscan/install.sh R2-11-6

COPY ibek-support/calc/ calc/
RUN calc/install.sh R3-7-5

COPY ibek-support/motor/ motor/
RUN motor/install.sh R7-3

COPY ibek-support/motorMotorSim/ motorMotorSim/
RUN motorMotorSim/install.sh R1-2

COPY ibek-support/ADCore/ ADCore/
RUN ADCore/install.sh R3-13

COPY ibek-support/ADGenICam ADGenICam/
RUN ADGenICam/install.sh R1-9
COPY ibek-support/ADSimDetector ADSimDetector/
RUN ADSimDetector/install.sh R2-10

COPY ibek-support/modbus/ modbus/
RUN modbus/install.sh R3-3

COPY ibek-support/screen-epics-ioc screen-epics-ioc/
RUN screen-epics-ioc/install.sh v1.3.1

COPY ibek-support/motorNewport motorNewport/
RUN motorNewport/install.sh R1-2-1

# COPY ibek-support/easy-driver-epics/ easy-driver-epics/
# RUN easy-driver-epics/install.sh master

COPY ibek-support/mydevmodule mydevmodule          ### HERE
RUN mydevmodule/install.sh master				   ## HERE


# get the ioc source and build it
COPY ioc/ ${SOURCE_FOLDER}/ioc
RUN cd ${IOC} && ./install.sh && make

##### runtime preparation stage ################################################

FROM developer AS runtime_prep

# get the products from the build stage and reduce to runtime assets only
RUN ibek ioc extract-runtime-assets /assets ${SOURCE_FOLDER}/ibek*

##### runtime stage ############################################################

FROM ${REGISTRY}/epics-base-${TARGET_ARCHITECTURE}-runtime:${BASE} AS runtime

# get runtime assets from the preparation stage
COPY --from=runtime_prep /assets /

# install runtime system dependencies, collected from install.sh scripts
RUN ibek support apt-install-runtime-packages --skip-non-native

ENV TARGET_ARCHITECTURE ${TARGET_ARCHITECTURE}

CMD ["/bin/bash", "-c", "${IOC}/start.sh"]


Step 3 build docker image


building:

Build Image
git clone https://baltig.infn.it/epics-containers/infn-epics-ioc.git --recurse-submodules
cd infn-epics-ioc
docker build --build-arg TARGET_ARCHITECTURE="linux" --build-arg TARGETARCH="amd64" -t baltig.infn.it:4567/epics-containers/infn-epics-ioc:local .

building development:

Build Image
git clone https://baltig.infn.it/epics-containers/infn-epics-ioc.git --recurse-submodules
cd infn-epics-ioc
docker build -f Dockerfile.devel --build-arg TARGET_ARCHITECTURE="linux" --build-arg TARGETARCH="amd64" -t baltig.infn.it:4567/epics-containers/infn-epics-ioc:devel .

Step 4 motor yaml instance

Write a mymotor.yaml ibek instance of the support just created and built:

YAML instance
gioc_name: rfmotors
description: RF motors 

entities:

  - type: motorNewport.SMC100CreateController
    controllerName: NEWPORT001
    P: "SPARC:RF:"
    IP: 192.168.190.56
    TCPPORT: 4001
    numAxes: 1

  
  - type: motorNewport.motorAxis
    controller: NEWPORT001
    M: "m0"
    DESC: "Axis"
    ADDR: 0
    DLLM: -25
    DHLM: 25
    home: 1
    start: 10
    VELO: 1
test instance

the following lines will mount the current directory that contains mymotor.yaml in the container /epics/ioc/config and will expose CA ports, then run the support just created:

Run Image and test
docker run -p 5064:5064/udp -p 5064:5064/tcp -p 5065:5065/udp -p 5065:5065/tcp -p 5075:5075/tcp  -p 5076:5076/udp -it -v .:/epics/ioc/config -v .:/epics/ioc/config baltig.infn.it:4567/epics-containers/infn-epics-ioc:local
root@3870c1890419:/epics/generic-source/ioc/config# /epics/ioc/start.sh
... io log
....
....

the following lines use the development image:

Run devel image
cd <myibek yaml instance>
docker run -p 5064:5064/udp -p 5064:5064/tcp -p 5065:5065/udp -p 5065:5065/tcp -p 5075:5075/tcp -p 5076:5076/udp -it -v .:/epics/ioc/config baltig.infn.it:4567/epics-containers/infn-epics-ioc:devel
root@3870c1890419:/epics/generic-source/ioc/config# /epics/ioc/start.sh
...
log
...

For test refer to existing ibek support