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.pyfile and adell/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.becomesdell,Hewlett Packard Enterprisebecomeshewlett,PowerEdge R650becomespoweredger650. 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, stopredhat/9.py, if yes use it, stopredhat/default.py, if yes use it, stopredhat.py, if yes use it, stopdefault.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.templredhat/9.templredhat/default.templredhat.templredhat9.pyredhat/9.pyredhat/default.pyredhat.pydefault.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.