From 53d66d84d8edbdd02fb5cdae836b671408f0b9c9 Mon Sep 17 00:00:00 2001
From: Erik von Reis <evonreis@caltech.edu>
Date: Tue, 1 Dec 2020 10:51:34 -0800
Subject: [PATCH] systemd_generator: generator boots all systems on large test
 stand.

---
 support/systemd/generator/cdsrfm.py    |  12 ++-
 support/systemd/generator/fe_generator |  11 +++
 support/systemd/generator/front_end.py | 128 ++++++++++++++++++-------
 support/systemd/generator/log.py       |   3 +
 support/systemd/generator/main.py      |   7 +-
 support/systemd/generator/options.py   |  42 +++-----
 support/systemd/generator/sequencer.py |  91 +++++++++++-------
 7 files changed, 192 insertions(+), 102 deletions(-)
 create mode 100755 support/systemd/generator/fe_generator
 create mode 100644 support/systemd/generator/log.py
 mode change 100644 => 100755 support/systemd/generator/main.py

diff --git a/support/systemd/generator/cdsrfm.py b/support/systemd/generator/cdsrfm.py
index 0f973010b..94aaad69d 100644
--- a/support/systemd/generator/cdsrfm.py
+++ b/support/systemd/generator/cdsrfm.py
@@ -3,10 +3,12 @@ from front_end import Process
 
 class CDSRFMProcesses(object):
     def __init__(self, target_dir):
-        self.target_dir=target_dir
+        self.target_dir = target_dir
 
-    def epics(self):
-        return Process(True, "rts-cdsrfm-epics", "rts-cdsrfm-epics")
+    @staticmethod
+    def epics():
+        return Process("rts-cdsrfm-epics.service", "rts-cdsrfm-epics.service")
 
-    def module(self):
-        return Process(True, "rts-cdsrfm-module", "rts-cdsrfm-module")
\ No newline at end of file
+    @staticmethod
+    def module():
+        return Process("rts-cdsrfm-module.service", "rts-cdsrfm-module.service")
diff --git a/support/systemd/generator/fe_generator b/support/systemd/generator/fe_generator
new file mode 100755
index 000000000..701f31c9d
--- /dev/null
+++ b/support/systemd/generator/fe_generator
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+source /etc/advligorts/env
+set -a
+source /etc/advligorts/systemd_env
+source /etc/advligorts/systemd_env_`hostname`
+set +a
+
+cd /usr/lib/fe_generator
+
+python3 main.py $@
\ No newline at end of file
diff --git a/support/systemd/generator/front_end.py b/support/systemd/generator/front_end.py
index e8b2e1d45..d8b056e9a 100644
--- a/support/systemd/generator/front_end.py
+++ b/support/systemd/generator/front_end.py
@@ -5,16 +5,20 @@ import os.path as path
 
 
 class Process(object):
-    def __init__(self, is_in_world, start, end):
-        self.is_in_world = is_in_world
+    def __init__(self, start, end, first_service=None):
         self.start = start
         self.end = end
+        if first_service is None:
+            self.first_service = start
+        else:
+            self.first_service = first_service
 
 
 class FrontEndProcesses(object):
     def __init__(self, target_dir):
         self.target_dir = target_dir
 
+    def models(self):
         target_name = path.join(self.target_dir, "rts-models.target")
         with open(target_name, "wt") as f:
             f.write("""[Unit]
@@ -23,52 +27,87 @@ Description=All models
 
     @staticmethod
     def dolphin_port():
-        return Process(False, 'rts-dolphin-port.service', 'rts-dolphin-port.service')
+        return Process('rts-dolphin-port.service', 'rts-dolphin-port.service')
 
     def dolphin_drivers(self):
         # link up from one to the next
+        targ_name = "rts-dolphin-driver.target"
+        targ_path = path.join(self.target_dir, targ_name)
         services = ['dis_kosif.service', 'dis_ix.service', 'dis_irm.service', 'dis_sisci.service',
                     'dis_nodemgr.service']
+        with open(targ_path, "wt") as f:
+            f.write(f"""[Unit]
+Description=Dolphin IX drivers
+Wants={" ".join(services)}""")
+
         self.serialize_units(services)
-        return Process(False, 'dis_kosif.service', 'dis_nodemgr.service')
+        return Process(targ_name, 'dis_nodemgr.service', 'dis_kosif.service',)
 
     def epics_only_models(self, models):
-        target_name = path.join(self.target_dir, "rts-epics-only-models.target")
-        with open(target_name, "wt") as f:
-            f.write("""[Unit]
-Description=All epics only models            
-""")
         services = [f"rts-epics@{model}.service" for model in models]
+
+        target_name = "rts-epics-only-models.target"
+        target_path = path.join(self.target_dir, target_name)
+        with open(target_path, "wt") as f:
+            f.write(f"""[Unit]
+Description=All epics only models
+Wants={" ".join(services)}            
+""")
+
         self.serialize_units(services)
         for service in services:
-            self.link_to(service, target_name)
+            self.part_of(service, target_name)
 
-        self.link_to("rts-epics-only-models.target", "rts-models.target")
+        self.link_to(target_name, "rts-models.target")
+        self.part_of(target_name, "rts-models.target")
 
-        return Process(True, "rts-epics-only-models.target", services[-1])
+        return Process(target_name, services[-1], services[0])
 
     def iop_model(self, model):
-        link_path = path.join(self.target_dir, "rts-iop-model.target")
-        os.symlink(f"rts@{model}.target", link_path)
+        target_path = path.join(self.target_dir, "rts-iop-model.target")
+        with open(target_path, "wt") as f:
+            f.write(f"""[Unit]
+Description=The IPO model.    
+Wants=rts@{model}.target                    
+""")
+
+        self.part_of(f"rts@{model}.target", "rts-iop-model.target")
 
         self.link_to("rts-iop-model.target", "rts-models.target")
+        self.part_of("rts-iop-model.target", "rts-models.target")
 
-        return Process(True, f"rts-iop-model.target", f"rts-awgtpman@{model}.service")
+        return Process(f"rts-iop-model.target", f"rts-awgtpman@{model}.service",
+                       f"rts-epics@{model}.service")
 
-    def user_models(self, models):
-        target_name = path.join(self.target_dir, "rts-user-models.target")
-        with open(target_name, "wt") as f:
+    def user_models(self, models, iop_model):
+        target_name = "rts-user-models.target"
+        target_path = path.join(self.target_dir, target_name)
+        with open(target_path, "wt") as f:
             f.write("""[Unit]
 Description=All user models            
 """)
-        for i in range(1, len(models)):
-            self.after(f"rts@{models[i]}.target", f"rts-epics@{models[i-1]}.service")
+
+        services = []
+        for model in models:
+            services += [f"rts-epics@{model}.service", f"rts-module@{model}.service",
+                         f"rts-awgtpman@{model}.service"]
+
+        self.serialize_units(services)
+
         for model in models:
             self.link_to(f"rts@{model}.target", target_name)
+            self.part_of(f"rts@{model}.target", target_name)
+
+            #make sure iop is running before running any user models
+            self.requires(f"rts-epics@{model}.service", f"rts-module@{iop_model}.service")
+            self.after(f"rts-epics@{model}.service", f"rts-module@{iop_model}.service")
+
+        self.link_to(target_name, "rts-models.target")
+        self.part_of(target_name, "rts-models.target")
 
-        self.link_to("rts-user-models.target", "rts-models.target")
 
-        return Process(True, f"rts-user-models.target", f"rts-awgtpman@{models[-1]}.service")
+        return Process(f"rts-user-models.target", services[-1],
+                       services[0])
 
     def edcs(self, edcs):
         """
@@ -76,7 +115,7 @@ Description=All user models
         """
         services = [f"rts-edc_{edc}.service" for edc in edcs]
         self.serialize_units(services)
-        return Process(True, services[0], services[-1])
+        return Process(services[0], services[-1])
 
     def streaming(self, options):
         # check transport specifier exists
@@ -85,7 +124,9 @@ Description=All user models
             raise Exception(f"option '{ts}' must be determined when reading options")
 
         # set up port
-        fpath = path.join(self.target_dir, "rts-daq.network")
+        network_dir = "/run/systemd/network"
+        os.makedirs(network_dir, exist_ok=True)
+        fpath = path.join(network_dir, "rts-daq.network")
         with open(fpath, "wt") as f:
             f.write(f"""[Match]
 Name={options['DAQ_ETH_DEV']}
@@ -113,8 +154,9 @@ Wants={" ".join(services)}
 
         for service in services:
             self.link_to(service, targ_unit_name)
+            self.part_of(service, targ_unit_name)
 
-        return Process(True, services[0], services[-1])
+        return Process(targ_unit_name, services[-1], services[0])
 
     def serialize_units(self, services):
         """
@@ -123,8 +165,15 @@ Wants={" ".join(services)}
         for i in range(1, len(services)):
             self.after(services[i], services[i-1])
 
+    def serialize_processes(self, processes):
+        """
+        Take a list of processes and put one after the other
+        """
+        for i in range(1, len(processes)):
+            self.after(processes[i].first_service, processes[i-1].end)
+
     def create_world_target(self):
-        with open(path.join(self.target_dir, "rts-world.target", "wt")) as world:
+        with open(path.join(self.target_dir, "rts-world.target"), "wt") as world:
             world.write("""[Unit]
 Description=All model and streaming services for Front End servers
 
@@ -138,11 +187,12 @@ WantedBy=multi-user.target
 
         :return:
         """
-        wants_path = path.join(self.target_dir, f"{target_name}.wants")
-        os.makedirs(wants_path, exist_ok=True)
-        targ = path.join("..", unit_name)
-        link = path.join(wants_path, unit_name)
-        os.symlink(targ, link)
+        override_path = path.join(self.target_dir, f"{target_name}.d")
+        os.makedirs(override_path, exist_ok=True)
+        conf_path = path.join(override_path, f"wants_{unit_name}.conf")
+        with open(conf_path, "wt") as f:
+            f.write(f"""[Unit]
+Wants={unit_name}""")
 
     def part_of(self, unit_name, parent_name):
         """
@@ -150,7 +200,7 @@ WantedBy=multi-user.target
         """
         unit_d = path.join(self.target_dir, f"{unit_name}.d")
         os.makedirs(unit_d, exist_ok=True)
-        fname = path.join(unit_d, f"part of {parent_name}.conf")
+        fname = path.join(unit_d, f"part_of_{parent_name}.conf")
         with open(fname, "wt") as conf:
             conf.write(f"""[Unit]
 PartOf={parent_name}
@@ -162,8 +212,20 @@ PartOf={parent_name}
         """
         after_d = path.join(self.target_dir, f"{after_name}.d")
         os.makedirs(after_d, exist_ok=True)
-        fname = path.join(after_d, f"after {before_name}.conf")
+        fname = path.join(after_d, f"after_{before_name}.conf")
         with open(fname, "wt") as conf:
             conf.write(f"""[Unit]
 After={before_name}
+""")
+
+    def requires(self, after_name, before_name):
+        """
+        Make the systemd unit after_name require before_name is finished
+        """
+        after_d = path.join(self.target_dir, f"{after_name}.d")
+        os.makedirs(after_d, exist_ok=True)
+        fname = path.join(after_d, f"require_{before_name}.conf")
+        with open(fname, "wt") as conf:
+            conf.write(f"""[Unit]
+Requires={before_name}
 """)
diff --git a/support/systemd/generator/log.py b/support/systemd/generator/log.py
new file mode 100644
index 000000000..5b985e769
--- /dev/null
+++ b/support/systemd/generator/log.py
@@ -0,0 +1,3 @@
+def klog(line):
+    with open("/dev/kmsg","wt") as f:
+        f.write(f"fe_generator: {line}\n")
\ No newline at end of file
diff --git a/support/systemd/generator/main.py b/support/systemd/generator/main.py
old mode 100644
new mode 100755
index 91956bdd5..ae7794f80
--- a/support/systemd/generator/main.py
+++ b/support/systemd/generator/main.py
@@ -6,7 +6,12 @@
 from sequencer import Sequencer
 from options import get_options
 import sys
+from log import klog
+
 
 if __name__ == '__main__':
-    seq = Sequencer(get_options(), sys.argv[1])
+    klog(f"in python with args {sys.argv}")
+    seq = Sequencer(get_options(), sys.argv[2])
+    klog(f"Sequencer created")
     seq.create_start_sequence()
+    klog(f"sequence complete")
diff --git a/support/systemd/generator/options.py b/support/systemd/generator/options.py
index a0b25e698..abba2d3d6 100644
--- a/support/systemd/generator/options.py
+++ b/support/systemd/generator/options.py
@@ -23,20 +23,21 @@ class Variable(object):
 # net every expected variable is set up here, only those that need translation from a string
 # or need a default value
 variables = {
-    'USER_MODELS': Variable(split),
-    'EPICS_ONLY_MODELS': Variable(split),
-    'EDC': Variable(split),
+    'USER_MODELS': Variable(split, default=[]),
+    'EPICS_ONLY_MODELS': Variable(split, default=[]),
+    'EDC': Variable(split, default=[]),
     'IS_DOLPHIN_NODE': Variable(boolean, False),
     'DAQ_STREAMING': Variable(boolean, False),
+    'CDSRFM': Variable(boolean, False),
+    'START_MODELS': Variable(boolean, False),
 }
 
 
 def get_options():
     # first check /etc/advligorts/systemd_env.. files
-    options = {}
-    read_env_file("/etc/advligorts/systemd_env", options)
+    options = {key: value for key,value in os.environ.items()}
     host_name = os.uname()[1]
-    read_env_file(f"/etc/advligorts/systemd_env_{host_name}", options)
+
 
     # also if rtsystab is available, prefer its unit list
 
@@ -82,40 +83,27 @@ def get_options():
     options['HAS_IOP_MODEL'] = 'IOP_MODEL' in options
     options['HAS_USER_MODELS'] = len(filtered_user_models) > 0
 
-    options['HAS_EDC'] = "EDCS" in options and len(options["EDCS"]) > 0
+    options['HAS_EDC'] = "EDC" in options and len(options["EDC"]) > 0
 
     if 'cps_xmit_args' in options:
         options['transport_specifier'] = 'cps_xmit'
 
-
-def read_env_file(fname, options):
-    try:
-        with open(fname, "rt") as f:
-            for line in f.readlines():
-                if line.strip()[0] == '#':
-                    continue
-                # ignore anything without an equal sign
-                if line.find('=') < 0:
-                    continue
-                pair = line.split('=')
-                key = pair[0].strip()
-                value = pair[1].strip()
-                options[key] = value
-    except IOError:
-        # don't do anything if file not there
-        pass
+    return options
 
 
 def read_rtsystab(fname, options, host_name):
     try:
         with open(fname, "rt") as f:
-            for line in f.readlines():
-                if line.strip()[0] == '#':
+            for line_raw in f.readlines():
+                line = line_raw.strip()
+                if len(line) <= 0:
+                    continue
+                if line[0] == '#':
                     continue
                 words = [word.strip() for word in line.split()]
                 if len(words) > 1 and words[0] == host_name:
                     options["IOP_MODEL"] = words[1]
-                    options["USER_MODELS"] = words[2:]
+                    options["USER_MODELS"] = " ".join(words[2:])
                     return
     except IOError:
         pass
diff --git a/support/systemd/generator/sequencer.py b/support/systemd/generator/sequencer.py
index fba228022..e882f9227 100644
--- a/support/systemd/generator/sequencer.py
+++ b/support/systemd/generator/sequencer.py
@@ -3,7 +3,8 @@
 from front_end import FrontEndProcesses
 from cdsrfm import CDSRFMProcesses
 import os.path as path
-import os
+from log import klog
+
 
 class Sequencer(object):
 
@@ -19,72 +20,90 @@ class Sequencer(object):
 
     def create_start_sequence(self):
         if not self.options["START_MODELS"]:
+            klog("START_MODELS is false. Quitting")
             return
         if self.options["CDSRFM"]:
-            sequence = self.create_cdsrfm_start_sequence(CDSRFMProcesses(self.target_dir))
+            klog("is CDSRFM host")
+            before_world, world = self.create_cdsrfm_start_sequence(self.processes,
+                                                         CDSRFMProcesses(self.target_dir))
         else:
-            sequence = self.create_frontend_start_sequence()
-        self.link_sequence(sequence)
+            klog("is standard front end host")
+            before_world, world = self.create_frontend_start_sequence()
+        self.link_sequence(before_world, world)
 
     def create_frontend_start_sequence(self):
-        sequence = []
+        before_world = []
+        world = []
+        models = False
         if self.options["HAS_DOLPHIN_PORT"] and self.options["IS_DOLPHIN_NODE"]:
-            sequence.append(self.processes.dolphin_port())
-            sequence.append(self.delay(15, "wait_for_dolphin_port"))
+            before_world.append(self.processes.dolphin_port())
+            before_world.append(self.delay(15, "dolphin_port"))
         if self.options["IS_DOLPHIN_NODE"]:
-            sequence.append(self.processes.dolphin_drivers())
-            sequence.append(self.delay(15), "wait_for_dolphin_driver")
+            before_world.append(self.processes.dolphin_drivers())
+        before_world.append(self.delay(30, "startup"))
         if self.options["HAS_EPICS_ONLY_MODELS"]:
-            sequence.append(self.processes.epics_only_models(self.options["EPICS_ONLY_MODELS"]))
+            world.append(self.processes.epics_only_models(self.options["EPICS_ONLY_MODELS"]))
+            models = True
         if self.options["HAS_IOP_MODEL"]:
-            sequence.append(self.processes.iop_model(self.options["IOP_MODEL"]))
+            world.append(self.processes.iop_model(self.options["IOP_MODEL"]))
+            models = True
         if self.options["HAS_EDC"]:
-            sequence.append(self.processes.edcs(self.options["EDCS"]))
+            world.append(self.processes.edcs(self.options["EDC"]))
         if self.options["DAQ_STREAMING"]:
-            sequence.append(self.processes.streaming(self.options))
+            world.append(self.processes.streaming(self.options))
         if self.options["HAS_USER_MODELS"]:
-            sequence.append(self.processes.user_models(self.options["USER_MODELS"]))
-        return sequence
+            if self.options["HAS_IOP_MODEL"]:
+                world.append(self.processes.user_models(self.options["USER_MODELS"],
+                                                        self.options["IOP_MODEL"]))
+                models = True
+            else:
+                klog("Can't have user models without an IOP model")
+        if models:
+            self.processes.models()
+        return before_world, world
 
-    @staticmethod
-    def create_cdsrfm_start_sequence(front_end_processes, cdsrfm_processes):
-        sequence = [
+    def create_cdsrfm_start_sequence(self, front_end_processes, cdsrfm_processes):
+        before_world = [
             front_end_processes.dolphin_port(),
+            self.delay(15, "dolphin_port"),
             front_end_processes.dolphin_drivers(),
+            self.delay(30, "startup"),]
+        world = [
+            cdsrfm_processes.module(),
             cdsrfm_processes.epics(),
-            cdsrfm_processes.module()]
-        return sequence
+            ]
+        return before_world, world
 
-    def link_sequence(self, sequence):
+    def link_sequence(self, before_world, world):
         self.processes.create_world_target()
 
         # link the first of each process to multi-user or to the world target
-        for process in sequence:
-            if process.is_in_world:
-                self.processes.wanted_by(process.start, "rts-world.target")
-                self.processes.part_of(process.start, "rts-world.target")
-            else:
-                self.processes.link_to(process.start, "multi-user.target")
+        for process in before_world:
+            self.processes.link_to(process.start, "multi-user.target")
+        for process in world:
+            self.processes.link_to(process.start, "rts-world.target")
+            self.processes.part_of(process.start, "rts-world.target")
+        if len(before_world) > 0:
+            self.processes.after("rts-world.target", before_world[-1].end)
+        self.processes.link_to("rts-world.target", "multi-user.target")
+        self.processes.serialize_processes(before_world + world)
 
-        for i in range(1, len(sequence)):
-            self.processes.after(sequence[i].start, sequence[i-1].end)
 
     class Delay(object):
-        is_in_world = False
-
         def __init__(self, unit_name):
             self.start = unit_name
             self.end = unit_name
+            self.first_service = unit_name
 
     def delay(self, time_s, name):
-        file_name = f"{name}.service"
+        file_name = f"rts-delay-{name}.service"
         file_path = path.join(self.target_dir, file_name)
         with open(file_path, "wt") as f:
-            f"""[Unit]
+            f.write(f"""[Unit]
 Description=Delay for {time_s} seconds
 
 [Service]
 ExecStartPre=/bin/sleep {time_s}
-ExecStart=/usr/bin/echo 'finished waiting for {name}'
-"""
-        return self.Delay(file_name)
\ No newline at end of file
+ExecStart=/bin/echo 'finished waiting for {name}'
+""")
+        return self.Delay(file_name)
-- 
GitLab