Luna daemon plugins

The luna daemon was designed with plugins in mind. The abstraction is being handled by the 'core' of the luna daemon, where the plugins are considered the arms and legs to 'communicate' with the outside 'luna daemon'-world.

The analogy was to allow administrators modifying the plugins to (further) tailor to their environment without changing the function of the daemon.

See also: for how the daemon loads plugins and selects them per node / hostname / group / distro, the segment-injection mechanism, and where plugins intersect with the boot, image and power flows, see luna2 daemon architecture.

List of plugins and templates

On the controller, plugins live under /trinity/local/luna/daemon/plugins/. Templates sit in the same tree, next to the plugin they stand in for.

  • plugins end with .py
  • templates end with .templ

The plugins shipped out of the box, per directory:

Directory Shipped Provides
auth/ local, pam, ldap the sources that authenticate a user, asked in the order the [AUTH] chain in luna.ini names them
boot/bmc/ default, dell, hewlett, hp, hpe, ibm, lenovo the BMC configuration snippet a node runs while it installs
boot/detection/ switchport, cloud how a booting node that is not known yet is identified
boot/network/ templates redhat8, redhat9, redhat10, ubuntu, opensuse; the .py plugins of the same names and default are deprecated the interface, gateway and hostname configuration of a node
boot/provision/ default, http, torrent, kickstart how a node fetches its image
boot/roles/ default the systemd units a role installs on a node
boot/scripts/ default, nodhcp, diskfull, raid1 pre, part and post install scripts
control/ default, redfish power, identify, SEL and boot override, through IPMI or Redfish
redfish/ default, gigabyte how a BMC's Redfish service is read and written, with vendor exceptions where a board needs them
export/ boot_plugins, prometheus, prometheus_rules, prometheus_hw_rules, prometheus_rules_settings the data sets the export API offers
import/ boot_plugins, prometheus_rules, prometheus_hw_rules, prometheus_rules_settings the data sets the import API accepts
hooks/config/ node, group, dns run after a node or group is created, changed, cloned or removed, and when DNS is rendered
hooks/control/ default run around a power or control action
hooks/luna/ default run when the daemon starts and stops
hooks/monitor/ node run when a node reports its state
osimage/filesystem/ default where an image lives on disk and how it is created, cloned, moved and removed
osimage/operations/image/ default, ubuntu packing an image into a kernel, an initrd and an image file
osimage/operations/osgrab/ default grabbing a running node into an image
osimage/operations/ospush/ default pushing an image onto a running node
osimage/other/ cleanup removing files that no image refers to any more
osuser/ obol the OS user and group backend
profile/delivery/ default, none how a profile bundle reaches a node

Plugins and templates can be added or modified to ones needs. The daemon notices a changed file on its own and reloads it on the next request, so no restart is needed.

How a plugin is found

Every lookup names a plugin directory, a list of candidate names in order of preference, and sometimes a model that narrows a candidate. The daemon tries the candidates left to right, and for each candidate it looks for four files, in this order:

<candidate><model>.py        the candidate and the model glued,      e.g. boot/bmc/dellpoweredger650.py
<candidate>/<model>.py       the model inside a candidate directory, e.g. boot/bmc/dell/poweredger650.py
<candidate>/default.py       the candidate directory's default,      e.g. boot/bmc/dell/default.py
<candidate>.py               the candidate on its own,               e.g. boot/bmc/dell.py

The first file that exists and defines a class Plugin wins. Only when no candidate produced a file is default.py in the directory used, so a candidate early in the list always beats one later in it, and default.py is reached only when nothing more specific exists. A lookup without a default.py to fall back on fails when no candidate matches.

Two things to know when naming a file:

  • use one form per name. A dell.py file and a dell/ directory side by side do not combine: Python imports one of them and the other is silently ignored.
  • a vendor and a model are derived from what the node's hardware inventory reports, normalised to a filename: lowercased, non-alphanumerics dropped, and for the vendor the first word only. Dell Inc. becomes dell, Hewlett Packard Enterprise becomes hewlett, PowerEdge R650 becomes poweredger650. Name the file after what the hardware reports, not after what the vendor is called.

Search path per lookup

The candidates in order, as the daemon builds them. Where a model is used it narrows every candidate as described above. Every lookup ends with default.py unless the row says otherwise.

Directory Candidates, in order Model Notes
auth/ each source named in the [AUTH] chain of luna.ini, one lookup per source no default; a source that is not there is skipped
boot/bmc/ node name, group name, vendor model runs while a node installs, so the vendor comes from an out-of-band Redfish collection; a node nobody collected resolves as node, group, default
boot/detection/ switchport, then cloud, each by name both are loaded; no search
boot/network/ distribution OS release a .templ file is searched first with the same candidates; the .py plugins are deprecated and tried only when no template matches, see below
boot/provision/ the provision method, then the provision fallback, each by name both are loaded and both download functions end up in the installer
boot/roles/ the role name one lookup per role listed on the node or group
boot/scripts/ node name, group name, distribution at install; a script listed on the node or group is additionally loaded by its name
control/ node name, group name, vendor, redfish model redfish is offered only when the BMC is known to speak Redfish; without that evidence a node falls through to default as it always did
redfish/ node name, group name, then the vendor for firmware and BIOS work model the Redfish action of luna control and the account setup stop at node, group, default
export/, import/ the requested name gated by ALLOWED_EXPORTERS and ALLOWED_IMPORTERS under [PLUGINS] in luna.ini; no default
hooks/config/ node, group or dns, by name no search
hooks/control/ node name, group name
hooks/luna/ default
hooks/monitor/ node, by name no search
osimage/filesystem/ IMAGE_FILESYSTEM under [PLUGINS] in luna.ini, else default
osimage/operations/image/ distribution OS release redhat + 9 tries redhat9.py, redhat/9.py, redhat/default.py, redhat.py, default.py
osimage/operations/osgrab/ node name, distribution, image name, group name
osimage/operations/ospush/ node name, distribution, image name, group name a push to a group omits the node name
osimage/other/ cleanup, by name no search
osuser/ obol, by name no search
profile/delivery/ node name, group name

Taking the image packing of an image that is redhat release 9 as the example, the daemon looks in osimage/operations/image/ for:

  • redhat9.py, if yes use it, stop
  • redhat/9.py, if yes use it, stop
  • redhat/default.py, if yes use it, stop
  • redhat.py, if yes use it, stop
  • default.py, if yes use it, stop
  • fail

The distribution and the release are the distribution and osrelease of the image:

# luna osimage show compute
+----------------------------------------------------------------------------------------------+
|                                         << Osimage >>                                        |
+---+---------+---------------------------+-------------------------+--------------+-----------+
| # |   name  |       kernelversion       |           path          | distribution | osrelease |
+---+---------+---------------------------+-------------------------+--------------+-----------+
| 1 | compute | 5.14.0-570.el9.x86_64     | /trinity/images/compute |    redhat    |     9     |
+---+---------+---------------------------+-------------------------+--------------+-----------+

Each plugin directory carries a README that states which methods or variables a plugin there has to define and the search path that applies to it.

Templates

The network configuration of a node comes from a template: a complete configuration file under boot/network/ that is rendered during the boot, searched with the same candidates and the same four forms but ending in .templ. The .py network plugins, which contribute bash snippets instead, are deprecated; they are tried only when no template matches, and default.py is what a distribution without a template falls back to. For a node whose image is redhat release 9 the complete order is:

  • redhat9.templ
  • redhat/9.templ
  • redhat/default.templ
  • redhat.templ
  • redhat9.py
  • redhat/9.py
  • redhat/default.py
  • redhat.py
  • default.py

There is no default.templ step: a distribution without a template falls through to the deprecated plugins.

Scripts and Roles plugins

Scripts and roles are powerful plugins used during the provisioning of nodes. These can be configured on a group or node level. Refer to Luna group and Luna node for information on how to set scripts and roles.

Example group configuration:

+-------------------------------------------------------------------------------+
|                                Group => compute                               |
+---------------------+---------------------------------------------------------+
| info                | Config differs from parent - local overrides            |
+---------------------+---------------------------------------------------------+
| name                | compute                                                 |
| domain              | cluster                                                 |
| osimage             | compute                                                 |
| osimagetag          | default (default)                                       |
| kerneloptions       | net.ifnames=0 biosdevname=0 (compute)                   |
| interfaces          | interface = BOOTIF                                      |
|                     |   network = cluster                                     |
|                     |   dhcp = True                                           |
|                     | interface = BMC                                         |
|                     |   network = ipmi                                        |
|                     |   dhcp = False                                          |
| setupbmc            | True                                                    |
| bmcsetupname        | compute                                                 |
| unmanaged_bmc_users | None                                                    |
| netboot             | True                                                    |
| bootmenu            | False (default)                                         |
| roles               | security-hardening, beegfs-server    <-- Default is None, see below
| scripts             | diskfull                             <-- Default is None, see below
+---------------------+---------------------------------------------------------+
| prescript           | <empty> (default)                                       |
+---------------------+---------------------------------------------------------+
| partscript          | mount -t tmpfs tmpfs /sysroot                           |
+---------------------+---------------------------------------------------------+
| postscript          | echo 'tmpfs / tmpfs defaults 0 0' >> /sysroot/etc/fstab |
+---------------------+---------------------------------------------------------+
| provision_interface | BOOTIF (default)                                        |
| provision_method    | http                                                    |
| provision_fallback  | http (cluster)                                          |
| comment             | None                                                    |
+---------------------+---------------------------------------------------------+

The defaults are None, meaning no scripts or roles are loaded. In the above example, one script and two roles are defined:

  • script: diskfull, a by us provided plugin (see above) that allows a node to do a diskfull installation without having to change the 'pre', 'part' and 'postscript' sections.
  • roles: security-hardening and beegfs-server. These are just examples and are not provided by default.

How 'scripts' work

Scripts are loaded or executed in addition to what's in the group's or node's respective 'pre', 'part' and 'postscript' section. The configuration in 'pre', 'part' and 'postscript' is being run prior to what's defined for 'scripts' in the group or node. In the above, partscript mount -t tmpfs tmpfs /sysroot is being executed prior to what's configured in the diskfull plugin section 'part'

The diskfull and raid1 scripts are legacy: they still ship and still work with install_mode: legacy, but a node installed to a local disk, a mirror or a RAID set is now declared with an install mode and a disk layout instead, see the node installer.

An example, boot/scripts/default.py:

class Plugin():
    """
    This is default class for pre, part, and post plugins.
    """

    def __init__(self):
        """
        prescript = runs before mounting sysroot during install
        partscript = runs before mounting sysroot during install
        postscript = runs before OS pivot
        """

    prescript = """
         # <--- bash commands go here....
    """

    partscript = """
         # or here....
    """

    postscript = """
         # and maybe here?
    """


How 'roles' work

A role is meant to extend an image or 'function' to fulfil a certain 'role'. This could be serving files, as in becoming a file server, or deploying additional configuration, or packages provided by another department. In short, items that cannot or should not be really inside an image or require a system to be up and running before it can be executed or deployed. As such, roles run after a node has booted up; it is being called for through systemd.

Roles is a perfect way for keeping images clean, or re-use images across different types or nodes/servers but also allow for delegation. E.g. department A is responsible for booting nodes, while department B has a recipe for deploying software, config, etc on top of that node.

An example, boot/roles/default.py:

class Plugin():
    """
    Class for a role.

    This plugin needs a mandatory variable set for template functionality
    -- script   --> This code will be called through the unit/systemd
                    The content will be written to /usr/local/roles/<role name>
    -- unit     --> This part provides the systemd unit or service file
    """


    script = """
#!/bin/bash
echo "Default example role" | logger
    """


    unit = """
[Unit]
Description=Luna Default example
After=multi-user.target

[Service]
Type=oneshot
ExecStart=/usr/local/roles/__LUNA_ROLE__

[Install]
WantedBy=multi-user.target
    """

The example will add the line 'Default example role' to the log file for the node.

Adding custom plugins and templates

The general rule would be to consult the README in the respective directory where the plugin is to be added. The README provides information about the naming convention, i.e. load order or search path and what functions and variables are required.

It also comes recommended to have a look at a default, e.g. default.py as it would provide additional information how the plugin could look like.

Note that each type of plugin comes with different requirements. As such plugins for network configs are inherently different compared to plugins that are loaded for osimage packing.