Skip to content

feat: school-qualified LINBO group ids in import-devices - #207

Open
TomlDev wants to merge 2 commits into
linuxmuster:feat/per-school-groupsfrom
TomlDev:feat/linbo-group-id
Open

TomlDev wants to merge 2 commits into
linuxmuster:feat/per-school-groupsfrom
TomlDev:feat/linbo-group-id

Conversation

@TomlDev

@TomlDev TomlDev commented Sep 28, 2026

Copy link
Copy Markdown

First of the small PRs for #206 (design in linuxmuster/linuxmuster-linbo7#178): the LINBO group id helper pair and its use in import_devices.py.

What

functions/linbo.py, re-exported from linuxmuster_base7.functions in the import list and in __all__:

  • getLinboGroupId(school, group) returns group for default-school and <school>+<group> for every other school.
  • splitLinboGroupId(group_id) returns ('default-school', group_id) when there is no +, otherwise it splits on the first +.

Neither part can contain +: sophomorix restricts group names to [A-Za-z0-9_-], and the school is part of every hostname that isValidHostname() checks. So the id round-trips without the school list. The helpers do no validation of their own.

cli/import_devices.py: the id is built right after each device row is unpacked, where school is already known. No function signature changes.

  • processDevicesForSubnet() → buildDhcpHostDeclaration(): option extensions-path / option nis-domain
  • doPxeGroupsBySchool(): the start.conf.<id> link source (start.conf-<ip|mac>), the ../<id>.cfg link source (hostcfg/<host>.cfg) and the returned pxe_groups
  • through pxe_groups: doLinboStartconf() (start.conf.<id>, including the unconfigured copy) and doGrubCfg() (<id>.cfg, and @@group@@ in the generated cfg, which only appears in echo lines)

The group column in devices.csv stays unqualified; the id is only derived.

Default-school stays identical

For default-school, getLinboGroupId() returns the group unchanged, so the DHCP config, the link csv, the symlinks and the file names are byte-for-byte the same as before. This is covered by tests that compare the exact output. Those default-school tests also pass against the unmodified import_devices.py from 7.4, and the non-default-school tests fail there as expected.

Deliberate non-changes

  • validateDeviceRow() / getDevicesArray() still return the plain group, although that's where the hostname gets its school prefix. linbo7's linbo_remote_lib.py (hostsInGroup(), linbo-remote -g <group> -s <school>) compares that column to the plain group the user types, and qualifying it there would break that.
  • Group = in a freshly created unconfigured start.conf copy is not rewritten. That belongs to the migration (next PR) and to linuxmuster-tools.
  • No other place in base7 derives a LINBO file name or DHCP option from the group. I grepped src/ and share/, including dhcpd-update-samba-dns.py and the dhcpd templates.

Not releasable on its own

Without the migration, the next import of a non-default school finds no start.conf.<school>+<group> and copies the unconfigured template, so that school's clients get a forced-netboot grub cfg. This PR should only go into a tagged release together with the migration PR (and the linbo7 linbo_sync fallback, linuxmuster/linuxmuster-linbo7#181). The proposal for where the migration runs is in #206.

Tests

  • tests/test_functions_linbo.py: both helpers, round-trips, group and school names with -/_, and the a+b-x vs. a-b+x collision case that ruled out -
  • tests/test_import_devices.py: processDevicesForSubnet() output, the link csv and pxe_groups from doPxeGroupsBySchool(), and an end-to-end run of generateGrubConfigsForGroups() (start.conf, grub cfg, symlinks), each for default-school and a non-default school. getDevicesArray() is stubbed, and the environment LINBO paths are monkeypatched to tmp_path.

pytest tests with linuxmuster-common's environment.py on sys.path: 55 passed (30 before). flake8 (--max-line-length=100, per the black config) reports nothing new in the changed files.

Refs #206, linuxmuster/linuxmuster-linbo7#178

@HappyBasher

Copy link
Copy Markdown
Member

Thanks, I reviewed this and ran the tests locally: 55 passed (30 on 7.4). The three default-school tests in test_import_devices.py also pass against the unmodified import_devices.py and the three non-default ones fail there, so the default-school output is indeed unchanged. The design and the deliberate non-changes (getDevicesArray() / validateDeviceRow(), Group =) look right to me.

Target branch. As you say in the description, this can't go into a release on its own: without the migration, a non-default school's clients get the forced-netboot grub cfg. 7.4 is our release branch, so any release from it would ship that state. I've created the branch feat/per-school-groups for the per-school group work. Please change the base of this PR to it. The migration PR and the other base7 parts go there as well. It is merged into 7.4 as a whole, together with its linbo7 counterpart, after the linbo7 release with linuxmuster-linbo7#181 (see #206).

7.4 has moved on since you opened the PR, so please rebase onto the new base. The three files you changed are untouched on 7.4, so there should be no conflicts.

Before merging, please change these in the commits:

  1. Add a Signed-off-by: trailer to both commits. It has to come from you as the author, so we can't add it for you. git rebase --signoff and git push --force-with-lease will do it. We merge contributor PRs with a merge commit, so the commit messages stay in the history.
  2. Replace Co-Authored-By: Claude <noreply@anthropic.com> with Assisted-by: Claude, as in your file headers.
  3. Update the Date in the file header of the three changed source files (cli/import_devices.py, functions/linbo.py, functions/__init__.py).

Optional hardening, as a separate small PR. The ids are unambiguous only because group names never contain +, and today only sophomorix guarantees that. linuxmuster-api will derive the owning school from the file name, so I'd rather have base7 enforce it as well: reject a group containing + in validateDeviceRow() (the device is skipped like any other invalid row), with a test. No need to bundle it with this PR.

@TomlDev
TomlDev changed the base branch from 7.4 to feat/per-school-groups October 1, 2026 10:12
@TomlDev
TomlDev force-pushed the feat/linbo-group-id branch from b84811e to 8f46af1 Compare October 1, 2026 10:12
On a multi-school server every LINBO hardware group is server-wide:
two schools with a group raum101 share one start.conf.raum101. As
agreed in linuxmuster-linbo7#178, every school other than
default-school gets the school-qualified group id <school>+<group>
for the LINBO artefacts derived from the group. default-school keeps
the plain group, so single-school installations see no change.

getLinboGroupId(school, group) builds the id, splitLinboGroupId()
reverses it: no '+' means default-school, otherwise split on the
first '+'. Neither part can contain '+' (sophomorix restricts group
names to [A-Za-z0-9_-], the school is part of every hostname that
isValidHostname() checks), so the id round-trips without the school
list. Both are re-exported from linuxmuster_base7.functions so that
linbo_mkgrubimg.py (linbo7) and linuxmuster-tools can reuse the
naming rule instead of duplicating it.

Added tests/test_functions_linbo.py.

Refs linuxmuster#206

Signed-off-by: Tom Lehmann <tom.lehmann@netzint.de>
Assisted-by: Claude
Use getLinboGroupId() wherever import-devices derives a LINBO name
from the device group:

- processDevicesForSubnet(): the group passed to
  buildDhcpHostDeclaration() for option extensions-path/nis-domain
- doPxeGroupsBySchool(): the start.conf.<id> and ../<id>.cfg link
  sources and the returned pxe_groups, which carry the id on to
  doLinboStartconf() (start.conf.<id>) and doGrubCfg() (<id>.cfg)

The id is built right after unpacking each device row, where the
school is already known, so no function signature changes.
default-school output (DHCP config, link csv, file names) stays byte
for byte the same.

The group is deliberately not qualified in validateDeviceRow()/
getDevicesArray(), where the hostname gets its school prefix:
linbo_remote_lib.py (linbo7) compares that group column to the
plain group given to linbo-remote -g <group> -s <school>.

Freshly created unconfigured start.conf copies keep their Group =
line; rewriting it belongs to the migration.

Added tests/test_import_devices.py. The default-school cases also
pass against the previous import_devices.py.

Refs linuxmuster#206

Signed-off-by: Tom Lehmann <tom.lehmann@netzint.de>
Assisted-by: Claude
@TomlDev
TomlDev force-pushed the feat/linbo-group-id branch from 8f46af1 to 4b962a3 Compare October 1, 2026 10:43
@TomlDev

TomlDev commented Oct 2, 2026

Copy link
Copy Markdown
Author

Thanks for the review. All points are in:

  • The base is now feat/per-school-groups, rebased onto it with no conflicts.
  • Both commits carry the trailers you asked for (sign-off, and the assistant trailer in place of the co-author line).
  • The Date in the file header of cli/import_devices.py, functions/linbo.py and functions/__init__.py is updated.

The optional hardening is #209: validateDeviceRow() rejects a group containing +, so the row is skipped like any other invalid row, with a test. It targets feat/per-school-groups as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants