IOC services

This guide covers how to install EPICS IOCs as a systemd service on a NixOS machine.

Pre-requisites

  • Having a NixOS machine with a flake configuration.

If you’re not sure how to do this, you can follow the Creating an Archiver Appliance instance tutorial, which is a good introduction on how to make a NixOS VM.

If you have such a configuration, make sure that:

  • You have the epnix flake input

  • You have added epnix as an argument to your flake outputs

  • You have imported EPNix’ NixOS module

For example:

flake.nix
 {
   # ...
   inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
+  inputs.epnix.url = "github:epics-extensions/EPNix/nixos-24.11";

   # ...
   outputs = {
     self,
     nixpkgs,
+    epnix,
   }: {
     nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
       modules = [
+        epnix.nixosModules.nixos

         # ...
       ];
     };
   };
 }

Exposing a service from your IOC

If your EPICS top and your NixOS configuration are in two different repositories, the recommended way to integrate your IOC is to add the configuration inside your EPICS top repository. This configuration will be exposed, so that you can use it inside your NixOS configuration repository.

From your EPICS top repository, make sure your flake.nix has these lines:

flake.nix — Exposed NixOS settings from your EPICS top
      overlays.default = final: _prev: {
        myIoc = final.callPackage ./ioc.nix {};
      };

      nixosModules.iocService = {config, ...}: {
        services.iocs.myIoc = {
          description = "An optional description of your IOC";
          package = self.packages.x86_64-linux.default;
          # Directory where to find the 'st.cmd' file
          workingDirectory = "iocBoot/iocMyIoc";
        };
      };

Make sure description and workingDirectory are correct. The workingDirectory must point to the directory containing the st.cmd file to run.

If you need a file other than st.cmd, see Custom cmd file.

See also

For a complete list of all IOC service-related options, see services.iocs.

Importing the exposed service

From your NixOS configuration repository, in your flake.nix, add your EPICS top as a flake input, and import the exposed service:

flake.nix — Importing the IOC service from your NixOS configuration
{
  # ...
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
  inputs.epnix.url = "github:epics-extensions/EPNix/nixos-24.11";
  inputs.myTop.url = "git+ssh://git@my-gitlab-server.com/EPICS/myTop.git";

  # ...
  outputs = {
    self,
    nixpkgs,
    epnix,
    myTop,
  }: {
    nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
      modules = [
        epnix.nixosModules.nixos
        myTop.nixosModules.iocService

        # ...
      ];
    };
  };
}

Then apply your NixOS configuration.

Adding an external IOC

As an alternative, if the IOC you want to run doesn’t expose a pre-configured service, or if you don’t want to use that configuration, you can define it directly in your NixOS configuration.

From your NixOS configuration repository, add your EPICS top to your flake inputs and overlays. For example:

flake.nix — Adding your top to your flake inputs and overlays
{
  # ...
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
  inputs.epnix.url = "github:epics-extensions/EPNix/nixos-24.11";
  inputs.myTop.url = "git+ssh://git@my-gitlab-server.com/EPICS/myTop.git";

  # ...
  outputs = {
    self,
    nixpkgs,
    epnix,
    myTop,
  }: {
    nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
      modules = [
        epnix.nixosModules.nixos
        # ...

        {
          nixpkgs.overlays = [
            (final: prev: {
              # Add 'myTop' to the set of 'pkgs'
              myTop = myTop.packages.x86_64-linux.default;
              # Add your other tops here, for example:
              #myOtherTop = myOtherTop.packages.x86_64-linux.default;
            })
          ];
        }
      ];
    };
  };
}

Then create an myIoc.nix file:

myIoc.nix — IOC configuration example
{ pkgs, ... }:
{
  # Replace 'myIoc' below with the name of your IOC
  services.iocs.myIoc = {
    description = "An optional description of your IOC";
    package = pkgs.myTop;
    # Directory where to find the 'st.cmd' file
    workingDirectory = "iocBoot/iocMyIoc";
  };
}

Make sure to import it in your flake.nix.

See also

For a list of all IOC-related options, see services.iocs.

Custom cmd file

If your IOC is started through a script other than a file st.cmd, set the option services.iocs.<name>.startupScript to you cmd script.

Custom procServ options

To change the procServ port, use services.iocs.<name>.procServ.port.

To change or add procServ options, use services.iocs.<name>.procServ.options.

For example:

Changing the default procServ options
  services.iocs.myIoc = {
    package = pkgs.myTop;
    # Directory where to find the 'st.cmd' file
    workingDirectory = "iocBoot/iocMyIoc";

    procServ = {
      # Set the port procServ listens to
      port = 2001;
      # Add an option `--killcmd "^b"`
      options.killcmd = "^b";
    };
  };

Passing environment variables

You can set environment variables for your IOC by using the option services.iocs.<name>.environment. For example:

Setting environment variables
  services.iocs.myIoc = {
    package = pkgs.myTop;
    workingDirectory = "iocBoot/iocMyIoc";

    environment.EPICS_CA_MAX_ARRAY_BYTES = 10000;
  };

Adding programs to the PATH

If your IOC calls external programs, you need to add those programs to your IOC’s PATH.

To do this, use the option services.iocs.<name>.path. For example:

Adding programs to the IOC’s PATH
{pkgs, ...}:
{
  services.iocs.myIoc = {
    package = pkgs.myTop;
    workingDirectory = "iocBoot/iocMyIoc";

    path = [pkgs.pciutils];
  };
}

Tip

Programs installed via the environment.systemPackages option are not available to systemd services.

Further customization

For other customization of IOC services, you can edit the generated systemd service by setting the options under systemd.services.myIoc.

For example, to make your machine reboot if your IOC fails to start:

Customizing the IOC systemd service
  services.iocs.myIoc = {
    package = pkgs.myTop;
    workingDirectory = "iocBoot/iocMyIoc";
  };

  # These options will modify the generated systemd service
  systemd.services.myIoc = {
    # These options will modify the [Unit] section
    # See `man systemd.unit` for available options in this section
    unitConfig = {
      # If the IOC reboot 5 times
      StartLimitBurst = 5;
      # in 30 seconds
      StartLimitIntervalSec = 30;
      # reboot
      StartLimitAction = "reboot";
    };
  };

For more information, examine the systemd.services options, and the man pages systemd.unit(5), systemd.service(5), and other related systemd documentation.

Systemd hardening

By default, the services.iocs module configures some systemd security hardening options. For example, the IOC can’t change the system clock, or change the machine’s hostname.

To examine the list of the enabled systemd hardening options, examine the nixos/modules/iocs.nix file in the EPNix source code.

You can turn off systemd hardening options by overriding the setting:

Turning off a systemd hardening option
  services.iocs.myIoc = {
    package = pkgs.myTop;
    workingDirectory = "iocBoot/iocMyIoc";
  };

  # These options will modify the generated systemd service
  systemd.services.myIoc = {
    # In the [Service] section,
    # ProtectClock was enabled by default,
    # but we override it here
    # to allow the IOC to change the system clock:
    serviceConfig.ProtectClock = false;
  };

For more information about hardening options, examine the man pages systemd.exec(5) and systemd.resource-control(5).