From 3dd37a071b73e3310d10739eeb1d020c96797cae Mon Sep 17 00:00:00 2001 From: Dorian Zedler Date: Sat, 15 Nov 2025 15:40:10 +0100 Subject: [PATCH 1/4] Feat: allow user to define custom template for share name --- .../linuxmusterLinuxclient7/config.py | 17 ++++++++ .../linuxmusterLinuxclient7/constants.py | 1 + .../linuxmusterLinuxclient7/gpo.py | 3 +- .../tests/files/config/config.no-shares.yml | 4 ++ .../tests/files/config/config.yml | 3 ++ .../tests/test_config.py | 28 +++++++++++++ .../linuxmusterLinuxclient7/tests/test_gpo.py | 41 +++++++++++++++++++ .../tests/test_user.py | 23 +++++++++++ .../linuxmusterLinuxclient7/user.py | 5 ++- 9 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/files/config/config.no-shares.yml diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/config.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/config.py index 5c380e0..ff26d70 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/config.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/config.py @@ -19,6 +19,23 @@ def network(): return True, networkConfig +def shares(): + """ + Get the shares configuration in `/etc/linuxmuster-linuxclient7/config.yml` + + :return: Tuple (success, dict of keys) + :rtype: tuple + """ + config = _readConfig() + sharesConfig = {} + if config is not None and "shares" in config: + sharesConfig = config["shares"] + + if not "nameTemplate" in sharesConfig: + sharesConfig["nameTemplate"] = constants.defaultShareNameTemplate + + return sharesConfig + def writeNetworkConfig(newNetworkConfig): """ Write the network configuration in `/etc/linuxmuster-linuxclient7/config.yml`. diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/constants.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/constants.py index 1cd5422..2cb8847 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/constants.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/constants.py @@ -10,6 +10,7 @@ shareMountBasepath = "/home/{}/media" hiddenShareMountBasepath = "/srv/samba/{}" machineAccountSysvolMountPath = "/var/lib/samba/sysvol" +defaultShareNameTemplate = "{label} ({letter}:)" etcBaseDir = "/etc/linuxmuster-linuxclient7" shareBaseDir = "/usr/share/linuxmuster-linuxclient7" diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/gpo.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/gpo.py index 7d00f4c..ffbb15e 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/gpo.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/gpo.py @@ -239,7 +239,8 @@ def _processDrivesPolicy(policyBasepath): for drive in shareList: if drive["useLetter"] == "1": - shareName = f"{drive['label']} ({drive['letter']}:)" + nameTemplate = config.shares()["nameTemplate"] + shareName = nameTemplate.format(label=drive['label'], letter=drive['letter']) else: shareName = drive["label"] diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/files/config/config.no-shares.yml b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/files/config/config.no-shares.yml new file mode 100644 index 0000000..8585e52 --- /dev/null +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/files/config/config.no-shares.yml @@ -0,0 +1,4 @@ +network: + serverHostname: server.linuxmuster.lan + domain: linuxmuster.lan + realm: LINUXMUSTER.LAN \ No newline at end of file diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/files/config/config.yml b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/files/config/config.yml index 8585e52..f4f29d4 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/files/config/config.yml +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/files/config/config.yml @@ -1,3 +1,6 @@ +shares: + nameTemplate: "{label}_{letter}" + network: serverHostname: server.linuxmuster.lan domain: linuxmuster.lan diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_config.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_config.py index 219553c..269d8c9 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_config.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_config.py @@ -44,6 +44,27 @@ def test_network_invalid(): assert not rc assert networkConfig is None +@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") +@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/config.yml") +def test_shares(): + sharesConfig = config.shares() + assert "nameTemplate" in sharesConfig + assert sharesConfig["nameTemplate"] == "{label}_{letter}" + +@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") +@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"/does/not/exist/config.yml") +def test_shares_none(): + sharesConfig = config.shares() + assert "nameTemplate" in sharesConfig + assert sharesConfig["nameTemplate"] == config.constants.defaultShareNameTemplate + +@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") +@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/config.no-shares.yml") +def test_shares_missing(): + sharesConfig = config.shares() + assert "nameTemplate" in sharesConfig + assert sharesConfig["nameTemplate"] == config.constants.defaultShareNameTemplate + @mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/config.invalid-syntax.yml") def test_syntax_invalid(): @@ -176,6 +197,13 @@ def test_upgrade_invalid(): assert os.path.exists(f"{os.path.dirname(os.path.realpath(__file__))}/files/config/network.invalid.conf") assert not os.path.exists("/tmp/config.yml") +@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/network.conf") +@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"/does/not/exist/config.yml") +def test_upgrade_unwritable(): + assert not config.upgrade() + assert os.path.exists(f"{os.path.dirname(os.path.realpath(__file__))}/files/config/network.invalid.conf") + assert not os.path.exists("/tmp/config.yml") + @mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"/does/not/exist/config.yml") def test_upgrade_nonexistent(): diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_gpo.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_gpo.py index 596e4e1..c22ef41 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_gpo.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_gpo.py @@ -46,6 +46,47 @@ def test_allOkAllTrue(mockUserSchool, mockLdapHelperSearchOne, mockSharesGetMoun assert len(mockPrintersInstallPrinter.call_args_list) == 1 assert mockPrintersInstallPrinter.call_args_list[0] == mock.call('ipp://SERVER/printers/PRINTER1', 'PRINTER1') +@mock.patch("linuxmusterLinuxclient7.gpo.config.shares") +@mock.patch("linuxmusterLinuxclient7.gpo.computer.isInGroup") +@mock.patch("linuxmusterLinuxclient7.gpo.user.isInGroup") +@mock.patch("linuxmusterLinuxclient7.gpo.printers.installPrinter") +@mock.patch("linuxmusterLinuxclient7.gpo.shares.mountShare") +@mock.patch("linuxmusterLinuxclient7.gpo.shares.getMountpointOfRemotePath") +@mock.patch("linuxmusterLinuxclient7.gpo.ldapHelper.searchOne") +@mock.patch("linuxmusterLinuxclient7.gpo.user.school") +def test_customShareNameTemplate(mockUserSchool, mockLdapHelperSearchOne, mockSharesGetMountpointOfRemotePath, mockSharesmMountShare, mockPrintersInstallPrinter, mockUserIsInGroup, mockComputerIsInGroup, mockConfigShares): + mockUserSchool.return_value = (True, "school1") + mockLdapHelperSearchOne.return_value = (True, { + "distinguishedName": "policy1", + "gPCFileSysPath": "\\\\linuxmuster.lan\\sysvol\\linuxmuster.lan\\Policies\\policy1" + }) + mockSharesGetMountpointOfRemotePath.return_value = (True, f"{os.path.dirname(os.path.realpath(__file__))}/files/policy1") + mockSharesmMountShare.return_value = (True, "") + mockPrintersInstallPrinter.return_value = True + mockUserIsInGroup.return_value = True + mockComputerIsInGroup.return_value = True + mockConfigShares.return_value = { + "nameTemplate": "{label}_{letter}" + } + + assert gpo.processAllPolicies() + assert len(mockSharesmMountShare.call_args_list) == 3 + assert mockSharesmMountShare.call_args_list[0] == mock.call('\\\\server\\default-school\\program', shareName='Programs_K') + # Projects (P:) is disabled and should not be mounted + assert mockSharesmMountShare.call_args_list[1] == mock.call('\\\\server\\default-school\\students', shareName='Students-Home_S') + assert mockSharesmMountShare.call_args_list[2] == mock.call('\\\\server\\default-school\\share', shareName='Shares') + + # Test without letter + mockConfigShares.return_value = { + "nameTemplate": "{label}" + } + assert gpo.processAllPolicies() + assert len(mockSharesmMountShare.call_args_list) == 6 + assert mockSharesmMountShare.call_args_list[3] == mock.call('\\\\server\\default-school\\program', shareName='Programs') + # Projects (P:) is disabled and should not be mounted + assert mockSharesmMountShare.call_args_list[4] == mock.call('\\\\server\\default-school\\students', shareName='Students-Home') + assert mockSharesmMountShare.call_args_list[5] == mock.call('\\\\server\\default-school\\share', shareName='Shares') + @mock.patch("linuxmusterLinuxclient7.gpo.computer.isInGroup") @mock.patch("linuxmusterLinuxclient7.gpo.user.isInGroup") @mock.patch("linuxmusterLinuxclient7.gpo.printers.installPrinter") diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_user.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_user.py index 266004f..02649b8 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_user.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_user.py @@ -135,6 +135,29 @@ def test_getHomeShareMountpoint(mockUsername, mockReadAttributes): rc, homeShareMountpoint = user.getHomeShareMountpoint() assert rc assert homeShareMountpoint == "/home/user1/media/user1 (H:)" + + +@mock.patch("linuxmusterLinuxclient7.gpo.config.shares") +@mock.patch("linuxmusterLinuxclient7.user.readAttributes") +@mock.patch("linuxmusterLinuxclient7.user.username") +def test_getHomeShareMountpointCustomShareNameTemplate(mockUsername, mockReadAttributes, mockConfigShares): + mockUsername.return_value = "user1" + mockReadAttributes.return_value = (True, {"homeDrive": "H:"}) + mockConfigShares.return_value = { + "nameTemplate": "{label}_{letter}" + } + + rc, homeShareMountpoint = user.getHomeShareMountpoint() + assert rc + assert homeShareMountpoint == "/home/user1/media/user1_H" + + # Test without letter + mockConfigShares.return_value = { + "nameTemplate": "{label}" + } + rc, homeShareMountpoint = user.getHomeShareMountpoint() + assert rc + assert homeShareMountpoint == "/home/user1/media/user1" @mock.patch("linuxmusterLinuxclient7.user.shares.mountShare") @mock.patch("linuxmusterLinuxclient7.user.readAttributes") diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/user.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/user.py index 3deae5d..2cc58e7 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/user.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/user.py @@ -146,8 +146,9 @@ def _getHomeShareName(userAttributes=None): if rc: try: - usernameString = username() - shareName = f"{usernameString} ({userAttributes['homeDrive']})" + nameTemplate = config.shares()["nameTemplate"] + letter = userAttributes['homeDrive'].replace(':', '') + shareName = nameTemplate.format(label=username(), letter=letter) return True, shareName except Exception as e: From bbe5c2a7bf0c7801b4aa35574c9eef4a88b32823 Mon Sep 17 00:00:00 2001 From: Dorian Zedler Date: Sat, 15 Nov 2025 15:58:29 +0100 Subject: [PATCH 2/4] Chore: update documentation --- wiki/Usage/Configuration.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/wiki/Usage/Configuration.md b/wiki/Usage/Configuration.md index ae4eb7c..f9b2666 100644 --- a/wiki/Usage/Configuration.md +++ b/wiki/Usage/Configuration.md @@ -1,10 +1,20 @@ # Shares -- Shares are configured via GPO and can be modified using gpedit on a Windows client -- If you don't have access to a Windows client, you can modify the file `/var/lib/samba/sysvol/linuxmuster.lan/Policies/{someUUID}/User/Preferences/Drives/Drives.xml` on the linuxmuster server directly. (Replace `someUUID` with the UUID of the policy) +- Shares are configured via GPO and can be modified by: + - using the linuxmuster-webui `client configuration -> Drives` menu + - using gpedit on a Windows client + - modifying the file `/var/lib/samba/sysvol/linuxmuster.lan/Policies/{someUUID}/User/Preferences/Drives/Drives.xml` on the linuxmuster server. (Replace `someUUID` with the UUID of the policy) +- If you want to customize the naming of shares which have drive letters, you can use the `nameTemplate` parameter in the `shares`-section of the config file (`/etc/linuxmuster-linuxclient7/config.yml`): + ```yaml + shares: + nameTemplate: "{label} ({letter}:)" + ``` + - Shares without drive letters always have the label as a name + - For the users home share, the label is the username # Printers - Printers MUST have the same name in cups and devices.csv! -- Printers can be assigned to groups in the AD +- To use a printer, either the computer or user must be member of the printers group + - Printer group membership can be controlled in the linuxmuster-webu `group membership` menu # Proxy server To configure a proxy server, add this to your logon.sh script: From d740ae28e57a328cd668431d0df192718dabe892 Mon Sep 17 00:00:00 2001 From: Dorian Zedler Date: Sat, 15 Nov 2025 16:09:16 +0100 Subject: [PATCH 3/4] Fix: only delete network config on setup/clean --- .../linuxmusterLinuxclient7/config.py | 12 +- .../linuxmusterLinuxclient7/setup.py | 6 +- .../tests/test_config.py | 112 +++++++++--------- 3 files changed, 65 insertions(+), 65 deletions(-) diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/config.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/config.py index ff26d70..1544ec0 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/config.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/config.py @@ -66,17 +66,19 @@ def upgrade(): """ return _upgrade() -def delete(): +def deleteNetworkConfig(): """ Delete the network configuration file. :return: True or False :rtype: bool """ - legacyNetworkConfigFleDeleted = fileHelper.deleteFile(constants.legacyNetworkConfigFilePath) - configFileDeleted = fileHelper.deleteFile(constants.configFilePath) - return legacyNetworkConfigFleDeleted and configFileDeleted - + config = _readConfig() + if config is None or "network" not in config: + return True + + del config["network"] + return _writeConfig(config) # -------------------- # - Helper functions - diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/setup.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/setup.py index c925200..2a03e79 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/setup.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/setup.py @@ -210,9 +210,9 @@ def _cleanOldDomainJoins(): if not fileHelper.deleteFilesWithExtension("/var/lib/samba/private/tls", ".pem"): return False - # remove configuration - logging.info(f"Deleting configuration files if exist ...") - if not config.delete(): + # remove network configuration + logging.info(f"Deleting network configuration ...") + if not config.deleteNetworkConfig(): return False return True diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_config.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_config.py index 269d8c9..da8d86b 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_config.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_config.py @@ -2,7 +2,9 @@ from unittest import mock from .. import config -@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/network.conf") +MOCK_FILE_PATH = f"{os.path.dirname(os.path.realpath(__file__))}/files/config" + +@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"{MOCK_FILE_PATH}/network.conf") @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"/does/not/exist/config.yml") def test_network_legacy(): rc, networkConfig = config.network() @@ -12,7 +14,7 @@ def test_network_legacy(): assert networkConfig["realm"] == "LINUXMUSTER.LEGACY" @mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/config.yml") -@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/config.yml") +@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{MOCK_FILE_PATH}/config.yml") def test_network(): rc, networkConfig = config.network() assert rc @@ -20,8 +22,8 @@ def test_network(): assert networkConfig["domain"] == "linuxmuster.lan" assert networkConfig["realm"] == "LINUXMUSTER.LAN" -@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/network.conf") -@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/config.yml") +@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"{MOCK_FILE_PATH}/network.conf") +@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{MOCK_FILE_PATH}/config.yml") def test_network_both(): rc, networkConfig = config.network() assert rc @@ -38,14 +40,14 @@ def test_network_none(): assert networkConfig is None @mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") -@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/config.invalid-network.yml") +@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{MOCK_FILE_PATH}/config.invalid-network.yml") def test_network_invalid(): rc, networkConfig = config.network() assert not rc assert networkConfig is None @mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") -@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/config.yml") +@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{MOCK_FILE_PATH}/config.yml") def test_shares(): sharesConfig = config.shares() assert "nameTemplate" in sharesConfig @@ -59,14 +61,14 @@ def test_shares_none(): assert sharesConfig["nameTemplate"] == config.constants.defaultShareNameTemplate @mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") -@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/config.no-shares.yml") +@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{MOCK_FILE_PATH}/config.no-shares.yml") def test_shares_missing(): sharesConfig = config.shares() assert "nameTemplate" in sharesConfig assert sharesConfig["nameTemplate"] == config.constants.defaultShareNameTemplate @mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") -@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/config.invalid-syntax.yml") +@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{MOCK_FILE_PATH}/config.invalid-syntax.yml") def test_syntax_invalid(): rc, networkConfig = config.network() assert not rc @@ -74,12 +76,8 @@ def test_syntax_invalid(): @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"/tmp/config.yml") def test_writeNetworkConfig(): - if os.path.exists("/tmp/config.yml"): - os.remove("/tmp/config.yml") - - with open(f"{os.path.dirname(os.path.realpath(__file__))}/files/config/config.yml", "r") as fsrc: - with open("/tmp/config.yml", "w") as fdst: - fdst.write(fsrc.read()) + _deleteFile("/tmp/config.yml") + _copyFile(f"{MOCK_FILE_PATH}/config.yml", "/tmp/config.yml") newNetworkConfig = { "serverHostname": "server.linuxmuster.new", @@ -93,17 +91,13 @@ def test_writeNetworkConfig(): assert networkConfig["serverHostname"] == "server.linuxmuster.new" assert networkConfig["domain"] == "linuxmuster.new" assert networkConfig["realm"] == "LINUXMUSTER.NEW" + assert config.shares() == {"nameTemplate": "{label}_{letter}"} - # TODO: once there are more config options, test that they are preserved @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"/tmp/config.yml") def test_writeNetworkConfig_invalid(): - if os.path.exists("/tmp/config.yml"): - os.remove("/tmp/config.yml") - - with open(f"{os.path.dirname(os.path.realpath(__file__))}/files/config/config.yml", "r") as fsrc: - with open("/tmp/config.yml", "w") as fdst: - fdst.write(fsrc.read()) + _deleteFile("/tmp/config.yml") + _copyFile(f"{MOCK_FILE_PATH}/config.yml", "/tmp/config.yml") newNetworkConfig = { "sserverHostname": "server.linuxmuster.new", @@ -130,8 +124,7 @@ def test_writeNetworkConfig_invalidPath(): @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"/tmp/config.yml") def test_writeNetworkConfig_empty(): - if os.path.exists("/tmp/config.yml"): - os.remove("/tmp/config.yml") + _deleteFile("/tmp/config.yml") newNetworkConfig = { "serverHostname": "server.linuxmuster.new", @@ -149,15 +142,9 @@ def test_writeNetworkConfig_empty(): @mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/tmp/network.conf") @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"/tmp/config.yml") def test_upgrade(): - if os.path.exists("/tmp/config.yml"): - os.remove("/tmp/config.yml") - - if os.path.exists("/tmp/network.conf"): - os.remove("/tmp/network.conf") - - with open(f"{os.path.dirname(os.path.realpath(__file__))}/files/config/network.conf", "r") as fsrc: - with open("/tmp/network.conf", "w") as fdst: - fdst.write(fsrc.read()) + _deleteFile("/tmp/config.yml") + _deleteFile("/tmp/network.conf") + _copyFile(f"{MOCK_FILE_PATH}/network.conf", "/tmp/network.conf") assert config.upgrade() assert not os.path.exists("/tmp/network.conf") @@ -173,8 +160,8 @@ def test_upgrade(): assert yamlContent["network"]["realm"] == "LINUXMUSTER.LEGACY" -@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/network.conf") -@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/config.yml") +@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"{MOCK_FILE_PATH}/network.conf") +@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{MOCK_FILE_PATH}/config.yml") def test_upgrade_alreadyUpToDate(): assert config.upgrade() @@ -187,21 +174,20 @@ def test_upgrade_alreadyUpToDate(): assert yamlContent["network"]["domain"] == "linuxmuster.lan" assert yamlContent["network"]["realm"] == "LINUXMUSTER.LAN" -@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/network.invalid.conf") +@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"{MOCK_FILE_PATH}/network.invalid.conf") @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"/tmp/config.yml") def test_upgrade_invalid(): - if os.path.exists("/tmp/config.yml"): - os.remove("/tmp/config.yml") + _deleteFile("/tmp/config.yml") assert not config.upgrade() - assert os.path.exists(f"{os.path.dirname(os.path.realpath(__file__))}/files/config/network.invalid.conf") + assert os.path.exists(f"{MOCK_FILE_PATH}/network.invalid.conf") assert not os.path.exists("/tmp/config.yml") -@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"{os.path.dirname(os.path.realpath(__file__))}/files/config/network.conf") +@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"{MOCK_FILE_PATH}/network.conf") @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"/does/not/exist/config.yml") def test_upgrade_unwritable(): assert not config.upgrade() - assert os.path.exists(f"{os.path.dirname(os.path.realpath(__file__))}/files/config/network.invalid.conf") + assert os.path.exists(f"{MOCK_FILE_PATH}/network.invalid.conf") assert not os.path.exists("/tmp/config.yml") @mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") @@ -209,21 +195,33 @@ def test_upgrade_unwritable(): def test_upgrade_nonexistent(): assert not config.upgrade() -@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/tmp/network.conf") +@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"/tmp/config.yml") -def test_delete(): - if os.path.exists("/tmp/network.conf"): - os.remove("/tmp/network.conf") - if os.path.exists("/tmp/config.yml"): - os.remove("/tmp/config.yml") - - with open(f"{os.path.dirname(os.path.realpath(__file__))}/files/config/network.conf", "r") as fsrc: - with open("/tmp/network.conf", "w") as fdst: - fdst.write(fsrc.read()) - with open(f"{os.path.dirname(os.path.realpath(__file__))}/files/config/config.yml", "r") as fsrc: - with open("/tmp/config.yml", "w") as fdst: - fdst.write(fsrc.read()) - - assert config.delete() - assert not os.path.exists("/tmp/network.conf") - assert not os.path.exists("/tmp/config.yml") \ No newline at end of file +def test_deleteNetworkConfig(): + _deleteFile("/tmp/network.conf") + _deleteFile("/tmp/config.yml") + + _copyFile(f"{MOCK_FILE_PATH}/config.yml", "/tmp/config.yml") + + assert config.deleteNetworkConfig() + assert os.path.exists("/tmp/config.yml") + assert config.network() == (False, None) + assert config.shares() == {"nameTemplate": "{label}_{letter}"} + +@mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") +@mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"/does/not/exist/config.yml") +def test_deleteNetworkConfig_nonexistent(): + assert config.deleteNetworkConfig() + +# -------------------- +# - Helper functions - +# -------------------- + +def _deleteFile(path): + if os.path.exists(path): + os.remove(path) + +def _copyFile(src, dst): + with open(src, "r") as fsrc: + with open(dst, "w") as fdst: + fdst.write(fsrc.read()) \ No newline at end of file From 7b17b431296212429492a6789129567b17563531 Mon Sep 17 00:00:00 2001 From: Dorian Zedler Date: Sat, 15 Nov 2025 16:27:39 +0100 Subject: [PATCH 4/4] Chore: use template only for letter --- .../linuxmusterLinuxclient7/config.py | 4 ++-- .../linuxmusterLinuxclient7/constants.py | 2 +- .../dist-packages/linuxmusterLinuxclient7/gpo.py | 4 ++-- .../tests/files/config/config.yml | 2 +- .../linuxmusterLinuxclient7/tests/test_config.py | 16 ++++++++-------- .../linuxmusterLinuxclient7/tests/test_gpo.py | 6 +++--- .../linuxmusterLinuxclient7/tests/test_user.py | 6 +++--- .../linuxmusterLinuxclient7/user.py | 4 ++-- wiki/Usage/Configuration.md | 10 ++++++---- 9 files changed, 28 insertions(+), 26 deletions(-) diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/config.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/config.py index 1544ec0..a7b0876 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/config.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/config.py @@ -31,8 +31,8 @@ def shares(): if config is not None and "shares" in config: sharesConfig = config["shares"] - if not "nameTemplate" in sharesConfig: - sharesConfig["nameTemplate"] = constants.defaultShareNameTemplate + if not "letterTemplate" in sharesConfig: + sharesConfig["letterTemplate"] = constants.defaultShareLetterTemplate return sharesConfig diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/constants.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/constants.py index 2cb8847..8ef7bfe 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/constants.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/constants.py @@ -10,7 +10,7 @@ shareMountBasepath = "/home/{}/media" hiddenShareMountBasepath = "/srv/samba/{}" machineAccountSysvolMountPath = "/var/lib/samba/sysvol" -defaultShareNameTemplate = "{label} ({letter}:)" +defaultShareLetterTemplate = " ({letter}:)" etcBaseDir = "/etc/linuxmuster-linuxclient7" shareBaseDir = "/usr/share/linuxmuster-linuxclient7" diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/gpo.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/gpo.py index ffbb15e..d3d1984 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/gpo.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/gpo.py @@ -239,8 +239,8 @@ def _processDrivesPolicy(policyBasepath): for drive in shareList: if drive["useLetter"] == "1": - nameTemplate = config.shares()["nameTemplate"] - shareName = nameTemplate.format(label=drive['label'], letter=drive['letter']) + formattedLetter = config.shares()["letterTemplate"].format(letter=drive['letter']) + shareName = f"{drive['label']}{formattedLetter}" else: shareName = drive["label"] diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/files/config/config.yml b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/files/config/config.yml index f4f29d4..634c8a6 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/files/config/config.yml +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/files/config/config.yml @@ -1,5 +1,5 @@ shares: - nameTemplate: "{label}_{letter}" + letterTemplate: "_{letter}" network: serverHostname: server.linuxmuster.lan diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_config.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_config.py index da8d86b..576c224 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_config.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_config.py @@ -50,22 +50,22 @@ def test_network_invalid(): @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{MOCK_FILE_PATH}/config.yml") def test_shares(): sharesConfig = config.shares() - assert "nameTemplate" in sharesConfig - assert sharesConfig["nameTemplate"] == "{label}_{letter}" + assert "letterTemplate" in sharesConfig + assert sharesConfig["letterTemplate"] == "_{letter}" @mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"/does/not/exist/config.yml") def test_shares_none(): sharesConfig = config.shares() - assert "nameTemplate" in sharesConfig - assert sharesConfig["nameTemplate"] == config.constants.defaultShareNameTemplate + assert "letterTemplate" in sharesConfig + assert sharesConfig["letterTemplate"] == config.constants.defaultShareLetterTemplate @mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{MOCK_FILE_PATH}/config.no-shares.yml") def test_shares_missing(): sharesConfig = config.shares() - assert "nameTemplate" in sharesConfig - assert sharesConfig["nameTemplate"] == config.constants.defaultShareNameTemplate + assert "letterTemplate" in sharesConfig + assert sharesConfig["letterTemplate"] == config.constants.defaultShareLetterTemplate @mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"{MOCK_FILE_PATH}/config.invalid-syntax.yml") @@ -91,7 +91,7 @@ def test_writeNetworkConfig(): assert networkConfig["serverHostname"] == "server.linuxmuster.new" assert networkConfig["domain"] == "linuxmuster.new" assert networkConfig["realm"] == "LINUXMUSTER.NEW" - assert config.shares() == {"nameTemplate": "{label}_{letter}"} + assert config.shares() == {"letterTemplate": "_{letter}"} @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"/tmp/config.yml") @@ -206,7 +206,7 @@ def test_deleteNetworkConfig(): assert config.deleteNetworkConfig() assert os.path.exists("/tmp/config.yml") assert config.network() == (False, None) - assert config.shares() == {"nameTemplate": "{label}_{letter}"} + assert config.shares() == {"letterTemplate": "_{letter}"} @mock.patch("linuxmusterLinuxclient7.config.constants.legacyNetworkConfigFilePath", f"/does/not/exist/network.conf") @mock.patch("linuxmusterLinuxclient7.config.constants.configFilePath", f"/does/not/exist/config.yml") diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_gpo.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_gpo.py index c22ef41..b33fdef 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_gpo.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_gpo.py @@ -54,7 +54,7 @@ def test_allOkAllTrue(mockUserSchool, mockLdapHelperSearchOne, mockSharesGetMoun @mock.patch("linuxmusterLinuxclient7.gpo.shares.getMountpointOfRemotePath") @mock.patch("linuxmusterLinuxclient7.gpo.ldapHelper.searchOne") @mock.patch("linuxmusterLinuxclient7.gpo.user.school") -def test_customShareNameTemplate(mockUserSchool, mockLdapHelperSearchOne, mockSharesGetMountpointOfRemotePath, mockSharesmMountShare, mockPrintersInstallPrinter, mockUserIsInGroup, mockComputerIsInGroup, mockConfigShares): +def test_customShareLetterTemplate(mockUserSchool, mockLdapHelperSearchOne, mockSharesGetMountpointOfRemotePath, mockSharesmMountShare, mockPrintersInstallPrinter, mockUserIsInGroup, mockComputerIsInGroup, mockConfigShares): mockUserSchool.return_value = (True, "school1") mockLdapHelperSearchOne.return_value = (True, { "distinguishedName": "policy1", @@ -66,7 +66,7 @@ def test_customShareNameTemplate(mockUserSchool, mockLdapHelperSearchOne, mockSh mockUserIsInGroup.return_value = True mockComputerIsInGroup.return_value = True mockConfigShares.return_value = { - "nameTemplate": "{label}_{letter}" + "letterTemplate": "_{letter}" } assert gpo.processAllPolicies() @@ -78,7 +78,7 @@ def test_customShareNameTemplate(mockUserSchool, mockLdapHelperSearchOne, mockSh # Test without letter mockConfigShares.return_value = { - "nameTemplate": "{label}" + "letterTemplate": "" } assert gpo.processAllPolicies() assert len(mockSharesmMountShare.call_args_list) == 6 diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_user.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_user.py index 02649b8..9f98038 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_user.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/tests/test_user.py @@ -140,11 +140,11 @@ def test_getHomeShareMountpoint(mockUsername, mockReadAttributes): @mock.patch("linuxmusterLinuxclient7.gpo.config.shares") @mock.patch("linuxmusterLinuxclient7.user.readAttributes") @mock.patch("linuxmusterLinuxclient7.user.username") -def test_getHomeShareMountpointCustomShareNameTemplate(mockUsername, mockReadAttributes, mockConfigShares): +def test_getHomeShareMountpointCustomShareLetterTemplate(mockUsername, mockReadAttributes, mockConfigShares): mockUsername.return_value = "user1" mockReadAttributes.return_value = (True, {"homeDrive": "H:"}) mockConfigShares.return_value = { - "nameTemplate": "{label}_{letter}" + "letterTemplate": "_{letter}" } rc, homeShareMountpoint = user.getHomeShareMountpoint() @@ -153,7 +153,7 @@ def test_getHomeShareMountpointCustomShareNameTemplate(mockUsername, mockReadAtt # Test without letter mockConfigShares.return_value = { - "nameTemplate": "{label}" + "letterTemplate": "" } rc, homeShareMountpoint = user.getHomeShareMountpoint() assert rc diff --git a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/user.py b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/user.py index 2cc58e7..e34bafd 100644 --- a/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/user.py +++ b/usr/lib/python3/dist-packages/linuxmusterLinuxclient7/user.py @@ -146,9 +146,9 @@ def _getHomeShareName(userAttributes=None): if rc: try: - nameTemplate = config.shares()["nameTemplate"] letter = userAttributes['homeDrive'].replace(':', '') - shareName = nameTemplate.format(label=username(), letter=letter) + formattedLetter = config.shares()["letterTemplate"].format(letter=letter) + shareName = f"{username()}{formattedLetter}" return True, shareName except Exception as e: diff --git a/wiki/Usage/Configuration.md b/wiki/Usage/Configuration.md index f9b2666..4f2425a 100644 --- a/wiki/Usage/Configuration.md +++ b/wiki/Usage/Configuration.md @@ -3,13 +3,15 @@ - using the linuxmuster-webui `client configuration -> Drives` menu - using gpedit on a Windows client - modifying the file `/var/lib/samba/sysvol/linuxmuster.lan/Policies/{someUUID}/User/Preferences/Drives/Drives.xml` on the linuxmuster server. (Replace `someUUID` with the UUID of the policy) -- If you want to customize the naming of shares which have drive letters, you can use the `nameTemplate` parameter in the `shares`-section of the config file (`/etc/linuxmuster-linuxclient7/config.yml`): +- If you want to customize how drive letters are formatted, you can use the `letterTemplate` parameter in the `shares`-section of the config file (`/etc/linuxmuster-linuxclient7/config.yml`): ```yaml shares: - nameTemplate: "{label} ({letter}:)" + letterTemplate: " ({letter}:)" ``` - - Shares without drive letters always have the label as a name - - For the users home share, the label is the username + - The letter template is directly appended to the share label + - `"_({letter})"` results in `projects_(P)` + - `"_{letter}"` results in `projects_P` + - `""` (empty string) results in `projects` # Printers - Printers MUST have the same name in cups and devices.csv!