Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 40 additions & 7 deletions src/serverfs/usr/share/linuxmuster/linbo/linbo_mkgrubimg.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# linbo-remote/linbo_remote_cli.py.
# Signed-off by: thomas@linuxmuster.net
# Assisted by : Claude
# Date : 20260903
# Date : 20261001
#

import configparser
Expand Down Expand Up @@ -56,6 +56,30 @@ def getGrubEfiModules(efi_dir='/usr/lib/grub/x86_64-efi'):
return GRUB_COMMON_MODULES + ' ' + ' '.join(efi_modules)


def getSchoolFromDevicesFile(wsfile):
"""
School a workstations file belongs to, by sophomorix's path convention
(same as linuxmuster-base7's readDevicesCsv()): a non-default school's
file is <school>/<school>.devices.csv, default-school's is devices.csv.
"""
basename = os.path.basename(wsfile)
if basename.endswith('.devices.csv'):
return basename[:-len('.devices.csv')]
return 'default-school'


def getQualifiedHostname(school, hostname, csv_hostname):
"""
Host name as it appears in hostcfg and dhcp: unchanged for default-school,
<school>-<name> for another school. The name keeps the case of the
devices.csv row, because linuxmuster-import-devices writes the prefixed
entries that way (getHostname() returns the lowercased name).
"""
if school == 'default-school':
return hostname
return school + '-' + csv_hostname


def usage():
print('Purpose: linbo-mkgrubimg creates host specific image for grub network')
print('boot and stores it in /srv/linbo/boot/grub/hostcfg/<hostname>.img.')
Expand All @@ -68,7 +92,9 @@ def usage():
print(' workstations file.')
print(' -w <file>, --workstations=<file> : path to workstations file, default is')
print(' /etc/linuxmuster/sophomorix')
print(' /default-school/devices.csv.')
print(' /default-school/devices.csv. For')
print(' another school use its')
print(' <school>/<school>.devices.csv.')


def main(argv=None):
Expand All @@ -86,14 +112,14 @@ def main(argv=None):
# default values
setfilename = False
wsfile = environment.WIMPORTDATA
hostname = None
search = None

# evaluate options
for o, a in opts:
if o in ("-s", "--setfilename"):
setfilename = True
elif o in ("-n", "--name"):
hostname, hostrow = getHostname(wsfile, a)
search = a
elif o in ("-w", "--workstations"):
if os.path.isfile(a):
wsfile = a
Expand All @@ -106,10 +132,17 @@ def main(argv=None):
else:
assert False, "unhandled option"

# evaluate hostname
# evaluate hostname, only now that -w is known whatever the option order
hostname = None
if search is not None:
hostname, hostrow = getHostname(wsfile, search)
if hostname is None:
usage()
sys.exit(1)
# hosts of a non-default school carry a <school>- prefix in dhcp and
# hostcfg, as linuxmuster-import-devices writes them
school = getSchoolFromDevicesFile(wsfile)
hostname = getQualifiedHostname(school, hostname, hostrow[1])

# grub image filename
img = environment.LINBOGRUBDIR + '/hostcfg/' + hostname + '.img'
Expand Down Expand Up @@ -205,7 +238,7 @@ def main(argv=None):
foption = 'filename "' + imgrel + '"'
# modify workstations file
row_old = field1 + ';' + field2 + ';' + group + ';' + mac + ';' + ip + ';' + field6 + ';' + field7 + ';' + field8 + ';' + field9 + ';' + field10 + ';' + field11
row_new = field1 + ';' + hostname + ';' + group + ';' + mac + ';' + ip + ';' + field6 + ';' + field7 + ';' + foption + ';' + field9 + ';' + field10 + ';' + field11
row_new = field1 + ';' + field2 + ';' + group + ';' + mac + ';' + ip + ';' + field6 + ';' + field7 + ';' + foption + ';' + field9 + ';' + field10 + ';' + field11
rc, content = readTextfile(wsfile)
rc = writeTextfile(wsfile, content.replace(row_old, row_new), 'w')
# modify dhcp device entry
Expand All @@ -222,7 +255,7 @@ def main(argv=None):
if 'host ' + hostname in content:
# replace device entry with custom grub img path
row_old = re.findall('host ' + hostname + ' .*?(?=}|$)', content, re.DOTALL)[0]
row_new = 'host ' + hostname + ' {\n hardware ethernet ' + mac + ';\n fixed-address ' + ip + ';\n ' + foption + ';\n option host-name "' + hostname + '";\n option extensions-path "' + group + '";\n'
row_new = 'host ' + hostname + ' {\n hardware ethernet ' + mac + ';\n fixed-address ' + ip + ';\n ' + foption + ';\n option host-name "' + hostname + '";\n option extensions-path "' + group + '";\n option nis-domain "' + group + '";\n'
row_new = row_new.replace(' fixed-address 0.0.0.0;\n', '')
rc = writeTextfile(item, content.replace(row_old, row_new), 'w')
break
Expand Down
63 changes: 63 additions & 0 deletions tests/python/test_linbo_mkgrubimg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#
# Filename : test_linbo_mkgrubimg.py
# Description : pytest tests for linbo_mkgrubimg.py's pure helpers - the
# school lookup from the workstations file path and the school
# qualified host name (issue #178).
# Signed-off by: tom.lehmann@netzint.de
# Assisted by : Claude
# Date : 20261001
#

import importlib
import sys
import types

import pytest


@pytest.fixture
def mkgrubimg(monkeypatch):
"""
linbo_mkgrubimg imports linuxmuster-base7 at module level, which isn't
installed in the test environment - stub the names it imports (none of
which the helpers under test call), scoped to the test so the stub doesn't
leak into tests that rely on base7 being absent.
"""
base7 = types.ModuleType('linuxmuster_base7')
functions = types.ModuleType('linuxmuster_base7.functions')
for name in ('getHostname', 'getStartconfOption', 'readTextfile', 'writeTextfile'):
setattr(functions, name, None)
base7.functions = functions
monkeypatch.setitem(sys.modules, 'linuxmuster_base7', base7)
monkeypatch.setitem(sys.modules, 'linuxmuster_base7.functions', functions)
monkeypatch.delitem(sys.modules, 'linbo_mkgrubimg', raising=False)
return importlib.import_module('linbo_mkgrubimg')


def test_default_school_devices_file(mkgrubimg):
assert mkgrubimg.getSchoolFromDevicesFile('/etc/linuxmuster/sophomorix/default-school/devices.csv') == 'default-school'


def test_non_default_school_devices_file(mkgrubimg):
assert mkgrubimg.getSchoolFromDevicesFile('/etc/linuxmuster/sophomorix/abc/abc.devices.csv') == 'abc'


def test_school_name_with_hyphen(mkgrubimg):
assert mkgrubimg.getSchoolFromDevicesFile('/etc/linuxmuster/sophomorix/gs-nord/gs-nord.devices.csv') == 'gs-nord'


def test_relative_path(mkgrubimg):
assert mkgrubimg.getSchoolFromDevicesFile('abc.devices.csv') == 'abc'
assert mkgrubimg.getSchoolFromDevicesFile('devices.csv') == 'default-school'


def test_default_school_host_name_is_unchanged(mkgrubimg):
assert mkgrubimg.getQualifiedHostname('default-school', 'pc01', 'PC01') == 'pc01'


def test_non_default_school_host_name_keeps_the_case_of_the_devices_row(mkgrubimg):
assert mkgrubimg.getQualifiedHostname('agy', 'pc02', 'PC02') == 'agy-PC02'


def test_non_default_school_host_name_with_lowercase_row(mkgrubimg):
assert mkgrubimg.getQualifiedHostname('gs-nord', 'pc02', 'pc02') == 'gs-nord-pc02'
Loading