Repository navigation
Per-school LINBO hardware groups: school-qualified group ids in import-devices #206
Description
This is the linuxmuster-base7 part of linuxmuster/linuxmuster-linbo7#178 (per-school hardware groups on multi-school servers). The design and cross-repo overview stay in linbo7#178. This issue tracks only the base7 work.
Background
On a multi-school server every hardware group is server-wide: two schools with a group raum101 share one start.conf.raum101, and school admins cannot safely be given control over "their" groups. As discussed in linbo7#178, the fix is a school-qualified group id that linuxmuster-import-devices generates. The LINBO client, the GRUB templates and the rsync hooks already pass the group string through unchanged, so they need no changes.
Target behaviour
- For every school other than
default-school, import-devices uses<school>__<group>in place of<group>for every LINBO artefact it derives from the group, e.g.start.conf.abc__raum101andabc__raum101.cfg. default-schoolkeeps the current flat names exactly as they are. Single-school installations see no change.
Separator __ (decided in linbo7#178): school names cannot contain _, because the school is part of every device hostname and isValidHostname() only allows [a-z0-9-]. Group names must not contain __. So a name without __ belongs to default-school, and a name with __ splits on the first __ into school and group, without the school list. A single _ in group names stays allowed.
+ was the first choice, but BusyBox udhcpc on the client sanitises DHCP option 40 (nis-domain) and only lets letters, digits, -, _ and . through; everything else arrives as bad (see linbo7#178).
Affected code (src/linuxmuster_base7/cli/import_devices.py)
buildDhcpHostDeclaration():option extensions-path/option nis-domaindoLinboStartconf():start.conf.<group>, including the unconfigured default copydoGrubCfg():<group>.cfgdoPxeGroupsBySchool(): thestart.conf-<ip>andhostcfg/<host>.cfglinks and the returned group list
The group column in devices.csv stays unqualified. The qualified id is derived, never stored.
Tasks
Each task is meant to be a separate, small PR:
- Group id helper pair in
linuxmuster_base7.functions: one function for school + group → qualified id and one for qualified id → school + group, so the naming rule lives in one place, with the separator defined once as a constant infunctions/linbo.py.linbo_mkgrubimg.py(linbo7) and linuxmuster-tools reuse them. Fordefault-schoolthe id is the plain group. Use them at all the call sites above. (feat: school-qualified LINBO group ids in import-devices #207) - Reject groups containing
__invalidateDeviceRow(), using the constant fromfunctions/linbo.py. (fix(import_devices): reject a device group containing '+' #209) - Migration of existing installations, lazily inside import-devices (
doLinboStartconf()): whenstart.conf.<school>__<group>is missing, copystart.conf.<group>, but only for groups that the school's previousboot/links/<school>.csvreferenced. Rewrite theGroup =line in the copy to the qualified id (clients use it asHOSTGROUPin offline mode);setGlobalStartconfOption()has to acceptGroup=without spaces for that. Copy an unmanaged<group>.cfgto<school>__<group>.cfg. Idempotent, never overwrites an existing qualified file. - Changelog / release notes: the new naming; formerly shared groups are split per school; custom postsync/prestart scripts see the new
$HOSTGROUP; the plain files stay behind; edits tostart.conf.<group>no longer reach a migrated school's clients; groups containing__are rejected.
Covered outside base7, see linbo7#178: the linbo_sync fallback for group-keyed Windows boot files (BCD, bsmbr, gptlabel) in existing images, and the linbo_mkgrubimg.py fixes for non-default schools.
Constraints
- Default-school behaviour must stay byte-for-byte identical (DHCP config, file names, links).
Release
All base7 PRs go into feat/per-school-groups. It is merged into 7.4 together with the matching linbo7 branch (linbo_mkgrubimg.py), after the linbo7 release with the linbo_sync fallback (linbo7#181) has reached the clients.
Contributing
PRs are welcome. Please keep each PR small and limited to one task above, so it can be reviewed and merged on its own. PRs that reformat existing code without a functional reason will not be accepted.
Related: linuxmuster/linuxmuster-linbo7#178, linuxmuster/linuxmuster-tools#36
Activity
TomlDev commentedon Sep 28, 2026
Where the migration runs: proposal
We'd like to propose running it lazily inside import-devices, as part of the per-group step, rather than on package upgrade or as an explicit command.
How
In
doLinboStartconf(), which after PR 1 (#207) receives the qualified id:start.conf.<school>+<group>does not exist yet butstart.conf.<group>does, copy the plain file (instead of the unconfiguredstart.conftemplate), then rewriteGroup =in the copy to the qualified id with the existingsetGlobalStartconfOption().<group>.cfgexists and is unmanaged (noMANAGEDSTR), copy it to<school>+<group>.cfgsodoGrubCfg()keeps treating it aspresent. Managed cfgs need no copy because they are regenerated anyway.splitLinboGroupId()from PR 1 gives the plain group back from the id, so this is roughly ten lines in one function, with no new parameters.Why there
writeDhcpDevicesConfig()followed by the group step and only then the DHCP restart). So the qualified files exist before any client gets the new name. A school that is never re-imported keeps its old names and old files, which is consistent.start.conf.<group>in between would then silently not reach that school after the switch.Downsides we found
abcadds a groupraum101long after the upgrade and default-school (or a not-yet-migrated file) hasstart.conf.raum101,abcgets a copy of that instead of the unconfigured template. If you'd rather avoid that, we can limit the copy to groups that the school's previousboot/links/<school>.csvreferenced asstart.conf.<group>. import-devices would read that file beforedoPxeGroupsBySchool()rewrites it. After the first migrated run it only references qualified names, so the copy then never fires again. The cost is a few more lines and threading that set through.echolines there, and GRUB takes the group from DHCP, so this should be harmless. But a custom cfg that hard-codes the plain group somewhere would keep it.setGlobalStartconfOption()only matchesGroup =with exactly one space before the=. ForGroup=raum101it inserts a secondGroup =line directly after[LINBO], above the old one, instead of replacing it.linbo_split_startconfkeeps the line order and the client sources/conf/linbo, so the old plain value wins and offline mode keeps the unqualified group. So the regex has to become tolerant (\s*=) in the same PR, not just as a nice-to-have.start.conf.<group>any more, it stays behind as an orphan and tools list it as a default-school group. Deleting it automatically seems riskier than documenting it.start.conf.<group>no longer reach the school's clients. That goes into the changelog together with the shared-group split.isfile()per group.$HOSTGROUP. This is independent of where the migration runs, but it belongs in the release notes. The shippedexamples/image.postsyncsuggests$HOSTGROUPas the name of a server-side patch directory. A script that does that looks in…/abc+raum101/after the migration and silently skips its patches, until the directory is renamed or the script uses the part after the+.Group-keyed sidecars that linuxmuster-tools manages (backups,
.vdi) are not touched here. They belong to the tools side.Release coupling
PR 1 alone must not ship in a release. Without the migration, the next import of a non-default school finds no
start.conf.<school>+<group>and copies the unconfigured template.doGrubCfg()then writesgrub.cfg.forced_netboot, so every client of that school is forced into netboot until someone re-creates its start.conf. Unmanaged grub cfgs would also be bypassed. We'd suggest merging PR 1 and PR 2 into 7.4 before the next tag, together with the linbo7linbo_syncfallback (linuxmuster/linuxmuster-linbo7#181), so migrated schools can still restore existing Windows images (BIOS and EFI).If this works for you, PR 2 will implement it in
doLinboStartconf(), with tests for the copy, theGroup =rewrite, the unmanaged cfg and the no-overwrite case.HappyBasher commentedon Oct 1, 2026
Release order for the migration: linuxmuster-linbo7#181 (the
linbo_syncfallback to the plain group for the Windows boot filesBCD,bsmbrandgptlabelin existing images) is a linbofs change. It has to reach the clients before the migration in this issue runs. Otherwise a migrated client of a non-default school syncing an existing Windows image finds only the files with the plain group name, skips the BCD, boot sector and GPT label restore, and on EFI Windows may not boot.So the linbo7 release with #181 comes first, and the base7 migration must not ship in the same step or earlier. This also matters for the open question about where the migration runs: if it runs on package upgrade, the linbo7 package with the new linbofs has to be installed and the linbofs rebuilt first (or the migration has to check for it).
feat(functions/linbo): add getLinboGroupId()/splitLinboGroupId()
feat(import_devices): use school-qualified LINBO group ids
feat(functions/linbo): add getLinboGroupId()/splitLinboGroupId()
feat(import_devices): use school-qualified LINBO group ids
fix(import_devices): reject a device group containing '+'
HappyBasher commentedon Oct 14, 2026
Sorry for the late answer. We agree with running the migration lazily inside import-devices, as you proposed, with these conditions:
start.conf.<group>only for groups that the school's previousboot/links/<school>.csvreferenced. Otherwise a group a school adds later would get a copy of another school's start.conf instead of the unconfigured template.setGlobalStartconfOption()in the same PR. As you found,Group=raum101without spaces gets a secondGroup =line above the old one, and the client keeps the plain group in offline mode. The regex has to accept\s*=.$HOSTGROUP(the shippedexamples/image.postsyncuses it as a directory name), the plain files stay behind as orphans, edits tostart.conf.<group>no longer reach a migrated school's clients, formerly shared groups are split per school, and groups containing__are rejected.Copying unmanaged grub cfgs verbatim (your downside 2) is fine.
Separator: the qualified id is now
<school>__<group>, see the decision in linuxmuster/linuxmuster-linbo7#178. I've updated the description of this issue.Release order stays as noted above: the PRs go into
feat/per-school-groups(base7) and, forlinbo_mkgrubimg.py, a matching branch in linbo7. Both are merged into7.4together, after the linbo7 release with linuxmuster/linuxmuster-linbo7#181 has reached the clients. So PR 1 and PR 2 do not go into7.4before the next tag on their own.Please base PR 2 on
feat/per-school-groups, with tests for the copy, theGroup =rewrite (with and without spaces), the unmanaged cfg, the restriction to previously used groups and the no-overwrite case.