├── poetry.toml ├── tests ├── data │ ├── util │ │ ├── dhcpd │ │ │ ├── link_to_dhcpd1_1.conf │ │ │ └── dhcpd_1.conf │ │ ├── task │ │ │ ├── certificate │ │ │ │ ├── invalid.pem │ │ │ │ ├── corrupt.pem │ │ │ │ └── example.pem │ │ │ ├── smb.conf │ │ │ ├── sudoers │ │ │ │ └── sudoers_without_entries │ │ │ └── updatePackages │ │ │ │ ├── experimental.repo │ │ │ │ └── example_updater.conf │ │ ├── file │ │ │ ├── opsi-configed_4.0.7.1.3-2.opsi.zsync │ │ │ ├── opsi │ │ │ │ ├── opsi.conf │ │ │ │ ├── control_without_versions │ │ │ │ ├── control_with_empty_property_values │ │ │ │ ├── control_with_german_umlauts │ │ │ │ ├── control_with_special_characters_in_property │ │ │ │ └── control.toml │ │ │ ├── txtsetupoem_testdata_5.oem │ │ │ ├── inf_testdata_3.inf │ │ │ ├── inf_testdata_5.inf │ │ │ ├── txtsetupoem_testdata_3.oem │ │ │ └── inf_testdata_2.inf │ │ ├── fake_global.conf │ │ └── syncFiles │ │ │ └── librsyncSignature.txt │ ├── backend │ │ ├── dhcp_ki.conf │ │ ├── small_hwaudit.conf │ │ ├── testingproduct_23-42.opsi │ │ └── small_extended_hwaudit.conf │ ├── system │ │ └── posix │ │ │ └── dhclient.leases │ └── package_control_file │ │ ├── control.yml │ │ ├── changelog.txt │ │ └── control ├── __init__.py ├── test_backend_sqlite.py ├── test_config.py ├── test_util_file_archive.py ├── test_backend_file.py ├── Backends │ ├── __init__.py │ ├── SQLite.py │ ├── config.py.gitlabci │ ├── config.py.example │ └── MySQL.py ├── test_util_task_initialize_backend.py ├── test_system_posix_distribution.py ├── test_backend_jsonrpc.py ├── test_util_product.py ├── test_util_task_update_backend_data.py ├── test_util_task_cleanup_backend.py ├── test_util_wim.py ├── test_backend_backenddispatcher.py ├── test_backend_multithreading.py ├── test_util_task_update_backend_file.py ├── test_backend_modificationtracker.py ├── helpers.py ├── test_util_file_dhcpdconf.py ├── test_util_task_backup.py ├── test_util_file_opsi_opsirc.py ├── test_backend_methods.py ├── test_backend_extend_d_10_opsi.py ├── test_util_windows_drivers.py └── test_backend_extend_d_70_wan.py ├── data ├── backends │ ├── sqlite.conf │ ├── opsipxeconfd.conf │ ├── file.conf │ ├── jsonrpc.conf │ ├── mysql.conf │ ├── hostcontrol.conf │ └── dhcpd.conf └── backendManager │ ├── extend.d │ ├── 70_wan.conf │ ├── 10_wim.conf │ ├── 20_easy.conf │ └── 45_deprecated.conf │ ├── dispatch.conf │ └── acl.conf ├── .vscode ├── extensions.json └── settings.json ├── OPSI ├── Backend │ ├── Manager │ │ ├── __init__.py │ │ ├── Config.py │ │ └── Authentication │ │ │ ├── __init__.py │ │ │ ├── PAM.py │ │ │ └── NT.py │ ├── Base │ │ ├── __init__.py │ │ └── ModificationTracking.py │ ├── __init__.py │ ├── BackendManager.py │ ├── Backend.py │ ├── JSONRPC.py │ └── HostControlSafe.py ├── __init__.py ├── Exceptions.py ├── Types.py ├── Util │ ├── Task │ │ ├── UpdateBackend │ │ │ ├── __init__.py │ │ │ └── ConfigurationData.py │ │ ├── __init__.py │ │ └── ConfigureBackend │ │ │ └── __init__.py │ ├── Path.py │ ├── Log.py │ └── File │ │ └── Opsi │ │ └── Opsirc.py ├── Object.py ├── System │ └── util.py └── Config.py ├── .gitignore ├── opsi-dev-tool.yml ├── pyproject.toml ├── gettext ├── python-opsi.pot ├── python-opsi_en.po ├── python-opsi_da.po ├── python-opsi_it.po ├── python-opsi_fr.po ├── python-opsi_de.po ├── python-opsi_es.po ├── python-opsi_pl.po ├── python-opsi_nl.po └── python-opsi_ru.po └── .gitlab-ci.yml /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | 4 | -------------------------------------------------------------------------------- /tests/data/util/dhcpd/link_to_dhcpd1_1.conf: -------------------------------------------------------------------------------- 1 | dhcpd_1.conf -------------------------------------------------------------------------------- /tests/data/util/task/certificate/invalid.pem: -------------------------------------------------------------------------------- 1 | This is not a valid certificate. -------------------------------------------------------------------------------- /data/backends/sqlite.conf: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module = 'SQLite' 4 | config = { 5 | "database": "/var/lib/opsi/opsi.sqlite3", 6 | } 7 | -------------------------------------------------------------------------------- /data/backends/opsipxeconfd.conf: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module = 'OpsiPXEConfd' 4 | config = { 5 | "port": "/var/run/opsipxeconfd/opsipxeconfd.socket" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /tests/data/util/file/opsi-configed_4.0.7.1.3-2.opsi.zsync: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opsi-org/python-opsi-legacy/HEAD/tests/data/util/file/opsi-configed_4.0.7.1.3-2.opsi.zsync -------------------------------------------------------------------------------- /tests/data/util/file/opsi/opsi.conf: -------------------------------------------------------------------------------- 1 | [groups] 2 | fileadmingroup = myPCpatch 3 | readonly = myopsireadonlys 4 | # readonly = do_not_use 5 | 6 | [packages] 7 | use_pigz = False 8 | -------------------------------------------------------------------------------- /data/backends/file.conf: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module = 'File' 4 | config = { 5 | "baseDir": "/var/lib/opsi/config", 6 | "hostKeyFile": "/etc/opsi/pckeys", 7 | } 8 | 9 | -------------------------------------------------------------------------------- /data/backends/jsonrpc.conf: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module = 'JSONRPC' 4 | config = { 5 | "address": "", 6 | "username": "", 7 | "password": "" 8 | } 9 | 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.python", 4 | "ms-python.vscode-pylance", 5 | "ryanluker.vscode-coverage-gutters", 6 | "streetsidesoftware.code-spell-checker", 7 | "wmaurer.change-case", 8 | "mrorz.language-gettext" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /data/backends/mysql.conf: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module = 'MySQL' 4 | config = { 5 | "address": "127.0.0.1", 6 | "database": "opsi", 7 | "username": "opsi", 8 | "password": "opsi" 9 | } 10 | -------------------------------------------------------------------------------- /tests/data/backend/dhcp_ki.conf: -------------------------------------------------------------------------------- 1 | option voip-tftp-server code 150 = { ip-address, ip-address }; 2 | 3 | shared-network opsi { 4 | subnet 192.168.3.0 netmask 255.255.255.0 { 5 | group { 6 | next-server 192.168.3.33; 7 | filename "linux/pxelinux.0"; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /OPSI/Backend/Manager/__init__.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Backend managing components. 8 | """ 9 | -------------------------------------------------------------------------------- /tests/data/util/fake_global.conf: -------------------------------------------------------------------------------- 1 | # An Example global.conf 2 | # 3 | # comment = yes 4 | # 5 | ; Look at all the comments in here 6 | ; comment = yes 7 | ; 8 | this is not a comment but also has no assignment done 9 | comment = no 10 | keyword = value 11 | value with spaces = this works too 12 | advanced value = we even can include a = and it works -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | OPSI Python Library - Testcases. 8 | 9 | Various unittests to test functionality of python-opsi. 10 | """ 11 | -------------------------------------------------------------------------------- /OPSI/__init__.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | opsi python library. 8 | 9 | This module is part of the desktop management solution opsi 10 | (open pc server integration) http://www.opsi.org 11 | """ 12 | 13 | __version__ = "4.3.0.28" 14 | -------------------------------------------------------------------------------- /data/backends/hostcontrol.conf: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module = 'HostControl' 4 | config = { 5 | "opsiclientdPort": 4441, 6 | "hostRpcTimeout": 10, 7 | "resolveHostAddress": False, 8 | "maxConnections": 500, 9 | "broadcastAddresses": { 10 | # The format used is: : { : [port1, port2, ...]} 11 | "0.0.0.0/0": { 12 | "255.255.255.255": [7, 9, 12287] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /OPSI/Exceptions.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | OPSI Exceptions. 8 | Deprecated, use opsicommon.exceptions instead. 9 | """ 10 | 11 | from opsicommon.exceptions import * # noqa: F403 12 | 13 | 14 | class CommandNotFoundException(RuntimeError): 15 | pass 16 | -------------------------------------------------------------------------------- /OPSI/Types.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Type forcing features. 8 | 9 | This module contains various methods to ensure force a special type 10 | on an object. 11 | Deprecated, use opsicommon.types instead. 12 | """ 13 | 14 | from opsicommon.types import * # noqa: F403 15 | -------------------------------------------------------------------------------- /OPSI/Util/Task/UpdateBackend/__init__.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Functionality to update OPSI backends. 8 | 9 | .. versionadded:: 4.0.6.1 10 | """ 11 | 12 | 13 | class BackendUpdateError(RuntimeError): 14 | "This error indicates a problem during a backend update." 15 | -------------------------------------------------------------------------------- /tests/data/util/dhcpd/dhcpd_1.conf: -------------------------------------------------------------------------------- 1 | # option routers rtr-29.example.org; 2 | # } 3 | # pool { 4 | # allow members of "foo"; 5 | # range 10.17.224.10 10.17.224.250; 6 | # } 7 | # pool { 8 | # deny members of "foo"; 9 | # range 10.0.29.10 10.0.29.230; 10 | # } 11 | #} 12 | use-host-decl-names on; 13 | subnet 192.168.0.0 netmask 255.255.0.0 { 14 | group { 15 | next-server 192.168.20.80; 16 | filename "linux/pxelinux.0"; 17 | host bh-win7 { 18 | fixed-address 192.168.20.81; 19 | hardware ethernet 52:54:00:29:23:16; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /data/backends/dhcpd.conf: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module = 'DHCPD' 4 | 5 | localip = "127.0.0.1" 6 | for addr in socket.gethostbyname_ex(socket.getfqdn())[2]: 7 | localip = addr 8 | if not addr.startswith("127"): 9 | break 10 | 11 | config = { 12 | "dhcpdOnDepot": False, 13 | "dhcpdConfigFile": "/etc/dhcp/dhcpd.conf", 14 | "reloadConfigCommand": "sudo service isc-dhcp-server restart", 15 | "fixedAddressFormat": "IP", # or FQDN 16 | "defaultClientParameters": { "next-server": localip, "filename": "linux/pxelinux.0" } 17 | } 18 | -------------------------------------------------------------------------------- /tests/test_backend_sqlite.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing the opsi SQLite backend. 8 | """ 9 | 10 | import pytest 11 | 12 | 13 | def testInitialisationOfSQLiteBackendWithoutParametersDoesNotFail(): 14 | sqlModule = pytest.importorskip("OPSI.Backend.SQLite") 15 | SQLiteBackend = sqlModule.SQLiteBackend 16 | 17 | backend = SQLiteBackend() 18 | backend.backend_createBase() 19 | -------------------------------------------------------------------------------- /tests/data/util/file/opsi/control_without_versions: -------------------------------------------------------------------------------- 1 | [Package] 2 | depends: 3 | incremental: False 4 | 5 | [Product] 6 | type: localboot 7 | id: noversionshere 8 | name: Missing product version should not crash opsi-makeproductfile 9 | description: 10 | advice: 11 | priority: 0 12 | licenseRequired: False 13 | productClasses: 14 | setupScript: 15 | uninstallScript: 16 | updateScript: 17 | alwaysScript: 18 | onceScript: 19 | customScript: 20 | userLoginScript: 21 | 22 | [Changelog] 23 | prod-redmine-725 (1.0-1) testing; urgency=low 24 | 25 | * Initial package 26 | 27 | -- Foo Wed, 29 Mar 2017 17:59:42 +0000 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.venv 2 | /build 3 | /dist 4 | /python-opsi_*.dsc 5 | /python-opsi_*.tar.gz 6 | /python-opsi_*.deb 7 | pip-wheel-metadata 8 | *~ 9 | *.pyc 10 | *.pyo 11 | .settings 12 | .project 13 | .pydevproject 14 | *.egg-info 15 | *.sublime-* 16 | *.idea/ 17 | tests/Backends/config.py 18 | *.mo 19 | 20 | # QA files: 21 | .coverage 22 | coverage.xml 23 | nosetests.xml 24 | testreport-OPSI.xml 25 | 26 | build/ 27 | data/version 28 | debian/files 29 | debian/python-opsi.debhelper.log 30 | debian/python-opsi.postinst.debhelper 31 | debian/python-opsi.prerm.debhelper 32 | debian/python-opsi.substvars 33 | debian/python-opsi/ 34 | locale/ 35 | 36 | 37 | -------------------------------------------------------------------------------- /OPSI/Util/Path.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Functionality to work with paths. 8 | """ 9 | 10 | import os 11 | from contextlib import contextmanager 12 | 13 | 14 | @contextmanager 15 | def cd(path: str): 16 | "Change the current directory to `path` as long as the context exists." 17 | 18 | currentDir = os.getcwd() 19 | os.chdir(path) 20 | try: 21 | yield 22 | finally: 23 | os.chdir(currentDir) 24 | -------------------------------------------------------------------------------- /tests/data/system/posix/dhclient.leases: -------------------------------------------------------------------------------- 1 | lease { 2 | interface "eth0"; 3 | fixed-address 172.16.166.102; 4 | filename "linux/pxelinux.0"; 5 | option subnet-mask 255.255.255.0; 6 | option routers 172.16.166.1; 7 | option dhcp-lease-time 600; 8 | option dhcp-message-type 5; 9 | option domain-name-servers 172.16.166.1; 10 | option dhcp-server-identifier 172.16.166.1; 11 | option dhcp-renewal-time 300; 12 | option dhcp-rebinding-time 525; 13 | option host-name "win7client"; 14 | option domain-name "vmnat.local"; 15 | renew 3 2014/05/28 12:31:42; 16 | rebind 3 2014/05/28 12:36:36; 17 | expire 3 2014/05/28 12:37:51; 18 | } 19 | -------------------------------------------------------------------------------- /tests/data/util/task/smb.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | netbios name = PRODSERVER 3 | workgroup = WWWORK 4 | server string = %h DC (Samba) 5 | wins support = yes 6 | name resolve order = lmhosts host wins bcast 7 | interfaces = lo eth0 8 | bind interfaces only = yes 9 | 10 | null passwords = no 11 | hide dot files = yes 12 | 13 | socket options = TCP_NODELAY 14 | 15 | load printers = yes 16 | printing = cups 17 | printcap name = cups 18 | 19 | [printers] 20 | comment = All Printers 21 | browseable = no 22 | path = /tmp 23 | printable = yes 24 | public = yes 25 | writable = no 26 | create mode = 0700 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": false, 3 | "editor.renderWhitespace": "all", 4 | "files.trimTrailingWhitespace": true, 5 | "files.autoSave": "off", 6 | "editor.formatOnType": true, 7 | "editor.formatOnPaste": true, 8 | "editor.formatOnSave": true, 9 | "[python]": { 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll": "explicit", 12 | "source.organizeImports": "explicit" 13 | }, 14 | "editor.defaultFormatter": "charliermarsh.ruff" 15 | }, 16 | "python.linting.enabled": true, 17 | "python.linting.maxNumberOfProblems": 1000, 18 | "python.linting.mypyEnabled": true, 19 | "python.linting.mypyArgs": [ 20 | "--show-error-codes" 21 | ], 22 | "python.testing.pytestEnabled": true, 23 | "python.languageServer": "Pylance" 24 | } -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing opsi config module. 8 | """ 9 | 10 | from OPSI.Config import ( 11 | DEFAULT_DEPOT_USER, 12 | FILE_ADMIN_GROUP, 13 | OPSI_ADMIN_GROUP, 14 | OPSI_GLOBAL_CONF, 15 | OPSICONFD_USER, 16 | ) 17 | 18 | import pytest 19 | 20 | 21 | @pytest.mark.parametrize( 22 | "value", 23 | [ 24 | FILE_ADMIN_GROUP, 25 | OPSI_ADMIN_GROUP, 26 | DEFAULT_DEPOT_USER, 27 | OPSI_GLOBAL_CONF, 28 | OPSICONFD_USER, 29 | ], 30 | ) 31 | def testValueIsSet(value): 32 | assert value is not None 33 | assert value 34 | -------------------------------------------------------------------------------- /tests/data/util/file/opsi/control_with_empty_property_values: -------------------------------------------------------------------------------- 1 | [Package] 2 | version: 1 3 | depends: 4 | incremental: False 5 | 6 | [Product] 7 | type: localboot 8 | id: emptypropertyvalues 9 | name: Empty Property Values 10 | description: 11 | advice: 12 | version: 4.1 13 | priority: 0 14 | licenseRequired: False 15 | productClasses: 16 | setupScript: setup.ins 17 | uninstallScript: 18 | updateScript: 19 | alwaysScript: 20 | onceScript: 21 | customScript: 22 | userLoginScript: 23 | 24 | [ProductProperty] 25 | type: unicode 26 | name: important 27 | multivalue: False 28 | editable: True 29 | description: Nothing is important. 30 | values: [] 31 | default: [] 32 | 33 | [Changelog] 34 | emptypropertyvalues (4.1-1) testing; urgency=low 35 | 36 | * Initial package 37 | 38 | -- Jon Tue, 16 Aug 2006 15:56:24 +0000 39 | -------------------------------------------------------------------------------- /tests/data/util/file/opsi/control_with_german_umlauts: -------------------------------------------------------------------------------- 1 | [Package] 2 | version: 1 3 | depends: 4 | incremental: False 5 | 6 | [Product] 7 | type: localboot 8 | id: fix_druckerwarteschlange 9 | name: Druckerwarteschlange neustarten 10 | description: Startet die Druckerwarteschlange auf dem Client neu / oder überhaupt. 11 | advice: 12 | version: 1.0 13 | priority: 0 14 | licenseRequired: False 15 | productClasses: 16 | setupScript: setup.ins 17 | uninstallScript: 18 | updateScript: 19 | alwaysScript: 20 | onceScript: 21 | customScript: 22 | userLoginScript: 23 | 24 | [Changelog] 25 | fix_druckerwarteschlange (1.0-1) testing; urgency=low 26 | 27 | * Initial package 28 | 29 | -- Mike Krause Mon, 18 Feb 2013 11:34:08 +0000 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /OPSI/Backend/Base/__init__.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Backends. 8 | """ 9 | 10 | from __future__ import absolute_import 11 | 12 | from .Backend import Backend, describeInterface 13 | from .ConfigData import ConfigDataBackend 14 | from .Extended import ExtendedBackend, ExtendedConfigDataBackend 15 | from .ModificationTracking import ( 16 | BackendModificationListener, 17 | ModificationTrackingBackend, 18 | ) 19 | 20 | __all__ = ( 21 | "describeInterface", 22 | "Backend", 23 | "ExtendedBackend", 24 | "ConfigDataBackend", 25 | "ExtendedConfigDataBackend", 26 | "ModificationTrackingBackend", 27 | "BackendModificationListener", 28 | ) 29 | -------------------------------------------------------------------------------- /tests/data/util/task/certificate/corrupt.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBALPsdTWVnLk0VSXz 3 | FBx You have to let it all go, Neo. H9rNIp4AqqsgNPDXvjAFJQu9p+or 4 | /cB Fear, doubt, and disbelief. Free your mind. fYOm22MV2rHBWiHE 5 | EZ2vGnsov+kN6mpigMNpZHuu/yf+S7usQI/VMwUN7fwvla1SKTn00YjIbk5clI1r 6 | ukeocrRaYQY8 7 | -----END PRIVATE KEY----- 8 | -----BEGIN CERTIFICATE----- 9 | MIICgzCCAeygAwIBAgIJAP+/DMKNHd3YMA0GCSqGSIb3DQEBBQUAMHgxCzAJBgNV 10 | BAYTAkRFMQswCQYDVQQIEwJSUDEOMAwGA1UEBxMFTWFpbnoxDDAKBgNVBAoTA1VJ 11 | 0NEkylhfFlIJ2BIoCSz4uIDA7hbreaN9npvVgATNcINpNp3A0ejHAwIDAQABoxUw 12 | EzARBglghkgBhvhCAQEEBAMCBkAwDQYJKoZIhvcNAQEFBQADgYEAKgUN6IJ+/o3Y 13 | YzztnlN8I3aeH69+hn680LWiDOD3IFR4Bb2yeEG1F/3TiKn5Ppmw7pl+BHutfatK 14 | p/6kCD4NU+m9Fp0hgd+ffbt6EpZa6iuLSLouC+O4bqWhFTvIdYNYt1IQyg88EBM1 15 | jsW5eOUnSQz+hYIXG73XrZWU+r8zRYk= 16 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /tests/data/util/task/sudoers/sudoers_without_entries: -------------------------------------------------------------------------------- 1 | # 2 | # This file MUST be edited with the 'visudo' command as root. 3 | # 4 | # Please consider adding local content in /etc/sudoers.d/ instead of 5 | # directly modifying this file. 6 | # 7 | # See the man page for details on how to write a sudoers file. 8 | # 9 | Defaults env_reset 10 | Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 11 | 12 | # Host alias specification 13 | 14 | # User alias specification 15 | 16 | # Cmnd alias specification 17 | 18 | # User privilege specification 19 | root ALL=(ALL:ALL) ALL 20 | 21 | # Members of the admin group may gain root privileges 22 | %admin ALL=(ALL) ALL 23 | 24 | # Allow members of group sudo to execute any command 25 | %sudo ALL=(ALL:ALL) ALL 26 | 27 | # See sudoers(5) for more information on "#include" directives: 28 | 29 | #includedir /etc/sudoers.d 30 | -------------------------------------------------------------------------------- /opsi-dev-tool.yml: -------------------------------------------------------------------------------- 1 | project: 2 | licenses: 3 | - license: AGPL-3.0-only 4 | header: | 5 | python-opsi is part of the desktop management solution opsi http://www.opsi.org 6 | Copyright (c) 2008-{year} uib GmbH 7 | This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 8 | License: {license} 9 | 10 | locale: 11 | languages: 12 | - de 13 | - da 14 | - fr 15 | source_files: 16 | - OPSI/UI.py 17 | install: python-opsi_data/locale 18 | 19 | package: 20 | name: python3-opsi 21 | type: poetry 22 | depends: 23 | - python3-magic 24 | - python3-pampy 25 | - python3-pyasn1 26 | - python3-pycryptodome 27 | - python3-sqlalchemy 28 | - python3-mysqldb 29 | - python3-distro 30 | - python3-newt 31 | - python3-psutil 32 | - librsync | librsync2 | librsync1 33 | - iproute2 34 | - lshw 35 | 36 | -------------------------------------------------------------------------------- /OPSI/Util/Log.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Utilities for working with logs. 8 | """ 9 | 10 | from OPSI.Types import forceInt 11 | 12 | __all__ = ("truncateLogData",) 13 | 14 | 15 | def truncateLogData(data, maxSize): 16 | """ 17 | Truncating `data` to not be longer than `maxSize` chars. 18 | 19 | :param data: Text 20 | :type data: str 21 | :param maxSize: The maximum size that is allowed in chars. 22 | :type maxSize: int 23 | """ 24 | maxSize = forceInt(maxSize) 25 | dataLength = len(data) 26 | if dataLength > maxSize: 27 | start = data.find("\n", dataLength - maxSize) 28 | if start == -1: 29 | start = dataLength - maxSize 30 | return data[start:].lstrip() 31 | 32 | return data 33 | -------------------------------------------------------------------------------- /OPSI/Backend/__init__.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Backends. 8 | """ 9 | 10 | import functools 11 | from typing import Callable 12 | 13 | 14 | def no_export(func: Callable) -> Callable: 15 | func.no_export = True 16 | return func 17 | 18 | 19 | def deprecated( 20 | func: Callable = None, *, alternative_method: Callable = None 21 | ) -> Callable: 22 | if func is None: 23 | return functools.partial(deprecated, alternative_method=alternative_method) 24 | 25 | func.deprecated = True 26 | func.alternative_method = alternative_method 27 | return func 28 | 29 | # @functools.wraps(func) 30 | # def wrapper(*args, **kwargs): 31 | # logger.warning("Deprecated") 32 | # return func(*args, **kwargs) 33 | # return wrapper 34 | -------------------------------------------------------------------------------- /OPSI/Backend/BackendManager.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | BackendManager. 8 | 9 | If you want to work with an opsi backend in i.e. a script a 10 | BackendManager instance should be your first choice. 11 | A BackendManager instance does the heavy lifting for you so you don't 12 | need to set up you backends, ACL, multiplexing etc. yourself. 13 | """ 14 | 15 | from .Manager._Manager import BackendManager, backendManagerFactory 16 | from .Manager.AccessControl import BackendAccessControl 17 | from .Manager.Dispatcher import BackendDispatcher 18 | from .Manager.Extender import BackendExtender 19 | 20 | __all__ = ( 21 | "BackendManager", 22 | "BackendDispatcher", 23 | "BackendExtender", 24 | "BackendAccessControl", 25 | "backendManagerFactory", 26 | ) 27 | -------------------------------------------------------------------------------- /OPSI/Object.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | General classes used in the library. 8 | 9 | As an example this contains classes for hosts, products, configurations. 10 | 11 | Deprecated, use opsicommon.objects instead. 12 | """ 13 | 14 | from typing import Any 15 | 16 | from opsicommon.objects import * # noqa: F403 17 | 18 | mandatoryConstructorArgs = mandatory_constructor_args 19 | getIdentAttributes = get_ident_attributes 20 | getForeignIdAttributes = get_foreign_id_attributes 21 | getBackendMethodPrefix = get_backend_method_prefix 22 | getPossibleClassAttributes = get_possible_class_attributes 23 | decodeIdent = decode_ident 24 | 25 | 26 | def objectsDiffer(obj1: Any, obj2: Any, excludeAttributes: list[str] = None) -> bool: 27 | return objects_differ(obj1, obj2, exclude_attributes=excludeAttributes) 28 | -------------------------------------------------------------------------------- /data/backendManager/extend.d/70_wan.conf: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def changeWANConfig(self, boolean, clientIds): 4 | """ 5 | Change the WAN configuration. 6 | 7 | :param boolean: Should the WAN config be enabled or not? 8 | :type boolean: bool 9 | :param clientIds: The IDs of the clients where the setting should be changed. 10 | :type clientIDs: [str, ] 11 | """ 12 | try: 13 | forceHostIdList 14 | except NameError: 15 | from OPSI.Types import forceHostIdList 16 | 17 | try: 18 | forceBool 19 | except NameError: 20 | from OPSI.Types import forceBool 21 | 22 | enabled = forceBool(boolean) 23 | 24 | for clientId in forceHostIdList(clientIds): 25 | self.configState_create('opsiclientd.event_gui_startup.active', clientId, not enabled) 26 | self.configState_create('opsiclientd.event_gui_startup{user_logged_in}.active', clientId, not enabled) 27 | self.configState_create('opsiclientd.event_net_connection.active', clientId, enabled) 28 | self.configState_create('opsiclientd.event_timer.active', clientId, enabled) 29 | -------------------------------------------------------------------------------- /tests/test_util_file_archive.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing the work with archives. 8 | """ 9 | 10 | import pytest 11 | 12 | from OPSI.Util.File.Archive import Archive, is_pigz_available 13 | 14 | from .helpers import mock 15 | 16 | 17 | def testArchiveFactoryRaisesExceptionOnUnknownFormat(): 18 | with pytest.raises(Exception): 19 | Archive("no_filename", format="unknown") 20 | 21 | 22 | @pytest.mark.parametrize("fileFormat", ["tar", "cpio"]) 23 | def testCreatingArchive(fileFormat): 24 | Archive("no_file", format=fileFormat) 25 | 26 | 27 | def testRaisingExceptionIfFiletypeCanNotBeDetermined(): 28 | with pytest.raises(Exception): 29 | Archive(__file__) 30 | 31 | 32 | def testDisablingPigz(): 33 | """ 34 | Disabling the usage of pigz by setting PIGZ_ENABLED to False. 35 | """ 36 | with mock.patch("OPSI.Util.File.Opsi.OpsiConfFile.isPigzEnabled", lambda x: False): 37 | assert is_pigz_available() is False 38 | -------------------------------------------------------------------------------- /tests/data/util/file/opsi/control_with_special_characters_in_property: -------------------------------------------------------------------------------- 1 | [Package] 2 | version: 1 3 | depends: 4 | incremental: False 5 | 6 | [Product] 7 | type: localboot 8 | id: prod-1750 9 | name: Control file with path 10 | description: 11 | advice: 12 | version: 1.0 13 | priority: 0 14 | licenseRequired: False 15 | productClasses: 16 | setupScript: setup.ins 17 | uninstallScript: 18 | updateScript: 19 | alwaysScript: 20 | onceScript: 21 | customScript: 22 | userLoginScript: 23 | 24 | [ProductProperty] 25 | type: unicode 26 | name: target_path 27 | multivalue: False 28 | editable: True 29 | description: The target path 30 | values: ["C:\\temp\\my_target"] 31 | default: ["C:\\temp\\my_target"] 32 | 33 | [ProductProperty] 34 | type: unicode 35 | name: adminaccounts 36 | multivalue: False 37 | editable: True 38 | description: Windows account(s) to provision as administrators. 39 | values: ["Administrator", "domain.local\\Administrator", "BUILTIN\\ADMINISTRATORS"] 40 | default: ["Administrator"] 41 | 42 | [Changelog] 43 | prod-1750 (1.0-1) testing; urgency=low 44 | 45 | * Initial package 46 | 47 | -- Test User Sat, 27 May 2017 18:38:48 +0000 48 | -------------------------------------------------------------------------------- /tests/data/util/syncFiles/librsyncSignature.txt: -------------------------------------------------------------------------------- 1 | Die NASA konnte wieder ein Funksignal der Sonde New Horizons empfangen. Damit scheint sicher, dass das Manöver ein Erfolg war und nun jede Menge Daten zu erwarten sind. Bis die alle auf der Erde sind, wird es aber dauern. 2 | 3 | Die NASA feiert eine "historische Nacht": Die Sonde New Horizons ist am Zwergplaneten Pluto vorbeigeflogen und hat kurz vor drei Uhr MESZ wieder Kontakt mit der Erde aufgenommen. Jubel, rotweißblaue Fähnchen und stehende Ovationen prägten die Stimmung im John Hopkins Labor in Maryland. Digital stellten sich prominente Gratulanten ein, von Stephen Hawking mit einer Videobotschaft bis zu US-Präsident Barack Obama per Twitter. 4 | 5 | "Hallo Welt" 6 | 7 | Das erste Funksignal New Horizons nach dem Vorbeiflug am Pluto brachte noch keine wissenschaftlichen Ergebnisse oder neue Fotos, sondern Telemetriedaten der Sonde selbst. Das war so geplant. Aus diesen Informationen geht hervor, dass es New Horizons gut geht, dass sie ihren Kurs hält und die vorausberechnete Menge an Speichersektoren belegt ist. Daraus schließen die Verantwortlichen der NASA, dass auch tatsächlich wissenschaftliche Informationen im geplanten Ausmaß gesammelt wurden. -------------------------------------------------------------------------------- /tests/data/util/file/txtsetupoem_testdata_5.oem: -------------------------------------------------------------------------------- 1 | [Disks] 2 | d1 = "Promise FastTrak TX4310 Driver Diskette", \\fttxr5_O, \\ 3 | d2 = "Promise FastTrak TX4310 Driver Diskette", \\fttxr5_O, \\i386 4 | d3 = "Promise FastTrak TX4310 Driver Diskette", \\fttxr5_O, \\x86_64 5 | 6 | [Defaults] 7 | scsi = fttxr5_O_i386 8 | 9 | [scsi] 10 | fttxr5_O_i386 = "Promise FastTrak TX4310 (tm) Controller-Intel x86", fttxr5_O 11 | fttxr5_O_x86_64 = "Promise FastTrak TX4310 (tm) Controller-x86_64", fttxr5_O 12 | 13 | [Files.scsi.fttxr5_O_i386] 14 | driver = d2, fttxr5_O.sys, fttxr5_O 15 | ;driver = d2, bb_run.sys, bb_run 16 | ;driver = d1, DontGo.sys, dontgo 17 | ;dll = d1, ftutil2.dll 18 | inf = d2, fttxr5_O.inf 19 | catalog= d2, fttxr5_O.cat 20 | 21 | [Files.scsi.fttxr5_O_x86_64] 22 | driver = d3, fttxr5_O.sys, fttxr5_O 23 | ;driver = d3, bb_run.sys, bb_run 24 | ;driver = d1, DontGo.sys, dontgo 25 | ;dll = d1, ftutil2.dll 26 | inf = d3, fttxr5_O.inf 27 | catalog= d3, fttxr5_O.cat 28 | 29 | 30 | 31 | [HardwareIds.scsi.fttxr5_O_i386] 32 | id="PCI\VEN_105A", "fttxr5_O" 33 | 34 | [HardwareIds.scsi.fttxr5_O_x86_64] 35 | id="PCI\VEN_105A", "fttxr5_O" 36 | 37 | 38 | [Config.fttxr5_O] 39 | value = "", Tag, REG_DWORD, 1 -------------------------------------------------------------------------------- /tests/test_backend_file.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing the opsi file backend. 8 | """ 9 | 10 | import pytest 11 | 12 | from OPSI.Backend.File import FileBackend 13 | from OPSI.Exceptions import BackendConfigurationError 14 | 15 | from .Backends.File import getFileBackend 16 | 17 | 18 | def testGetRawDataFailsOnFileBackendBecauseMissingQuerySupport(): 19 | with getFileBackend() as backend: 20 | with pytest.raises(BackendConfigurationError): 21 | backend.getRawData("SELECT * FROM BAR;") 22 | 23 | 24 | def testGetDataFailsOnFileBackendBecauseMissingQuerySupport(): 25 | with getFileBackend() as backend: 26 | with pytest.raises(BackendConfigurationError): 27 | backend.getData("SELECT * FROM BAR;") 28 | 29 | 30 | @pytest.mark.parametrize( 31 | "filename", 32 | [ 33 | "exampleexam_e.-ex_1234.12-1234.12.localboot", 34 | ], 35 | ) 36 | def testProductFilenamePattern(filename): 37 | assert FileBackend.PRODUCT_FILENAME_REGEX.search(filename) is not None 38 | -------------------------------------------------------------------------------- /OPSI/Backend/Manager/Config.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | BackendManager configuration helper. 8 | """ 9 | 10 | import os 11 | import socket 12 | import sys 13 | from functools import lru_cache 14 | 15 | from OPSI.Exceptions import BackendConfigurationError 16 | 17 | 18 | def loadBackendConfig(path): 19 | """ 20 | Load the backend configuration at `path`. 21 | :param path: Path to the configuration file to load. 22 | :type path: str 23 | :rtype: dict 24 | """ 25 | if not os.path.exists(path): 26 | raise BackendConfigurationError(f"Backend config file '{path}' not found") 27 | 28 | moduleGlobals = { 29 | "config": {}, # Will be filled after loading 30 | "module": "", # Will be filled after loading 31 | "os": os, 32 | "socket": socket, 33 | "sys": sys, 34 | } 35 | 36 | exec(_readFile(path), moduleGlobals) 37 | 38 | return moduleGlobals 39 | 40 | 41 | @lru_cache(maxsize=None) 42 | def _readFile(path): 43 | with open(path, encoding="utf-8") as configFile: 44 | return configFile.read() 45 | -------------------------------------------------------------------------------- /tests/data/util/task/updatePackages/experimental.repo: -------------------------------------------------------------------------------- 1 | ; These repositories point to the experimental branch of opsi. 2 | 3 | [repository_uib_linux_experimental] 4 | description = opsi Linux Support (experimental packages) 5 | active = true 6 | baseUrl = http://download.uib.de 7 | dirs = opsi4.1/experimental/packages/linux/localboot/, opsi4.1/experimental/packages/linux/netboot/ 8 | autoInstall = false 9 | autoUpdate = true 10 | autoSetup = false 11 | ; Set Proxy handler like: http://10.10.10.1:8080 12 | proxy = 13 | 14 | [repository_uib_local_image_experimental] 15 | description = opsi Local Image Backup extension (experimental packages) 16 | active = false 17 | baseUrl = http://download.uib.de 18 | dirs = opsi4.1/experimental/packages/opsi-local-image/localboot/, opsi4.1/experimental/packages/opsi-local-image/netboot/ 19 | autoInstall = false 20 | autoUpdate = true 21 | autoSetup = false 22 | ; Set Proxy handler like: http://10.10.10.1:8080 23 | proxy = 24 | 25 | [repository_uib_windows_experimental] 26 | description = opsi Windows Support (experimental packages) 27 | active = false 28 | baseUrl = http://download.uib.de 29 | dirs = opsi4.1/experimental/packages/windows/localboot/, opsi4.1/experimental/packages/windows/netboot/ 30 | autoInstall = false 31 | autoUpdate = true 32 | autoSetup = false 33 | ; Set Proxy handler like: http://10.10.10.1:8080 34 | proxy = 35 | -------------------------------------------------------------------------------- /OPSI/Util/Task/__init__.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | opsi python library - Util - Task 8 | 9 | This module is part of the desktop management solution opsi 10 | (open pc server integration) http://www.opsi.org 11 | 12 | Copyright (C) 2006-2019 uib GmbH 13 | 14 | http://www.uib.de/ 15 | 16 | All rights reserved. 17 | 18 | This program is free software; you can redistribute it and/or modify 19 | it under the terms of the GNU General Public License version 2 as 20 | published by the Free Software Foundation. 21 | 22 | This program is distributed in the hope that it will be useful, 23 | but WITHOUT ANY WARRANTY; without even the implied warranty of 24 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 | GNU General Public License for more details. 26 | 27 | You should have received a copy of the GNU General Public License 28 | along with this program; if not, write to the Free Software 29 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 30 | 31 | @copyright: uib GmbH 32 | @author: Christian Kampka 33 | @license: GNU General Public License version 2 34 | """ 35 | -------------------------------------------------------------------------------- /tests/Backends/__init__.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Basics for backend tests. 8 | """ 9 | 10 | from contextlib import contextmanager 11 | 12 | from OPSI.Backend.Backend import ExtendedConfigDataBackend 13 | 14 | __all__ = ("getTestBackend", "BackendMixin") 15 | 16 | 17 | @contextmanager 18 | def getTestBackend(extended=False): 19 | """ 20 | Get a backend for tests. 21 | 22 | Each call to this will return a different backend. 23 | 24 | If `extended` is True the returned backend will be an 25 | `ExtendedConfigDataBackend`. 26 | """ 27 | from .File import getFileBackend # lazy import 28 | 29 | with getFileBackend() as backend: 30 | if extended: 31 | backend = ExtendedConfigDataBackend(backend) 32 | 33 | backend.backend_createBase() 34 | try: 35 | yield backend 36 | finally: 37 | backend.backend_deleteBase() 38 | 39 | 40 | class BackendMixin: 41 | """ 42 | Base class for backend test mixins. 43 | 44 | :param CREATES_INVENTORY_HISTORY: Set to true if the backend keeps a \ 45 | history of the inventory. This will affects tests! 46 | :type CREATES_INVENTORY_HISTORY: bool 47 | """ 48 | 49 | CREATES_INVENTORY_HISTORY = False 50 | 51 | def setUpBackend(self): 52 | pass 53 | 54 | def tearDownBackend(self): 55 | pass 56 | -------------------------------------------------------------------------------- /tests/Backends/SQLite.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | from contextlib import contextmanager 7 | 8 | import pytest 9 | 10 | try: 11 | from .config import SQLiteconfiguration 12 | except ImportError: 13 | SQLiteconfiguration = {} 14 | 15 | 16 | @contextmanager 17 | def getSQLiteBackend(**backendOptions): 18 | sqliteModule = pytest.importorskip("OPSI.Backend.SQLite") 19 | SQLiteBackend = sqliteModule.SQLiteBackend 20 | 21 | # Defaults and settings from the old fixture. 22 | # defaultOptions = { 23 | # 'processProductPriorities': True, 24 | # 'processProductDependencies': True, 25 | # 'addProductOnClientDefaults': True, 26 | # 'addProductPropertyStateDefaults': True, 27 | # 'addConfigStateDefaults': True, 28 | # 'deleteConfigStateIfDefault': True, 29 | # 'returnObjectsOnUpdateAndCreate': False 30 | # } 31 | # licenseManagement = True 32 | 33 | optionsForBackend = SQLiteconfiguration 34 | optionsForBackend.update(backendOptions) 35 | 36 | yield SQLiteBackend(**optionsForBackend) 37 | 38 | 39 | @contextmanager 40 | def getSQLiteModificationTracker(): 41 | sqliteModule = pytest.importorskip("OPSI.Backend.SQLite") 42 | trackerClass = sqliteModule.SQLiteObjectBackendModificationTracker 43 | 44 | yield trackerClass(database=":memory:") 45 | -------------------------------------------------------------------------------- /tests/data/util/file/opsi/control.toml: -------------------------------------------------------------------------------- 1 | [Package] 2 | version = 1 3 | # depends = 4 | incremental = false # lowercase f! 5 | 6 | [Product] 7 | type = "localboot" 8 | id = "prod-1750" 9 | name = "Control file with path" 10 | description = """This is some test description 11 | spanning over multiple lines. 12 | 13 | # Some markdown 14 | 15 | * this 16 | * is 17 | * a 18 | * list 19 | 20 | and this is a [link](https://www.uib.de/) 21 | """ 22 | advice = "" 23 | version = "1.0" 24 | priority = 0 25 | licenseRequired = false 26 | # productClasses = 27 | setupScript = "setup.ins" 28 | # uninstallScript = 29 | # updateScript = 30 | # alwaysScript = 31 | # onceScript = 32 | # customScript = 33 | # userLoginScript = 34 | 35 | [[ProductProperty]] 36 | type = "unicode" 37 | name = "target_path" 38 | multivalue = false 39 | editable = true 40 | description = "The target path" 41 | values = ["C:\\temp\\my_target"] 42 | default = ["C:\\temp\\my_target"] 43 | 44 | [[ProductProperty]] 45 | type = "unicode" 46 | name = "adminaccounts" 47 | multivalue = false 48 | editable = true 49 | description = "Windows account(s) to provision as administrators." 50 | values = ["Administrator", "domain.local\\Administrator", "BUILTIN\\ADMINISTRATORS"] 51 | default = ["Administrator"] 52 | 53 | [[ProductDependency]} 54 | action = "setup" 55 | requiredProduct = "l-system-update" 56 | requiredAction = "setup" 57 | requirementType = "before" 58 | 59 | [Changelog] 60 | changelog = """ 61 | prod-1750 (1.0-1) testing; urgency=low 62 | 63 | * Initial package 64 | 65 | -- Test User Sat, 27 May 2017 18:38:48 +0000 66 | """ -------------------------------------------------------------------------------- /OPSI/System/util.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | from __future__ import annotations 7 | 8 | import struct 9 | from uuid import UUID 10 | 11 | from cryptography import x509 12 | from opsicommon.logging import get_logger 13 | 14 | 15 | def _get_secure_boot_certificates_from_efivar_payload(data: bytes) -> list[x509.Certificate]: 16 | certs = [] 17 | offset = 0 18 | while offset < len(data): 19 | # EFI_SIGNATURE_LIST header 20 | # typedef struct _EFI_SIGNATURE_LIST { 21 | # GUID SignatureType 22 | # UINT32 SignatureListSize 23 | # UINT32 SignatureHeaderSize 24 | # UINT32 SignatureSize 25 | # } EFI_SIGNATURE_LIST; 26 | if offset + 28 > len(data): 27 | break 28 | 29 | sig_type_raw, list_size, header_size, sig_size = struct.unpack_from("<16sIII", data, offset) 30 | sig_type = UUID(bytes_le=sig_type_raw) 31 | if sig_type != UUID("a5c059a1-94e4-4aa7-87b5-ab155c2bf072"): 32 | continue 33 | 34 | list_end = offset + list_size 35 | offset = offset + 28 + header_size 36 | 37 | # Parse signature entries 38 | while offset + sig_size <= list_end: 39 | sig_data = data[offset : offset + sig_size] 40 | try: 41 | certs.append(x509.load_der_x509_certificate(sig_data[16:])) 42 | except Exception as err: 43 | get_logger("opsi.general").warning("Failed to parse secure boot certificate: %s", err) 44 | offset += sig_size 45 | 46 | # Safety: move to end of the list in case of padding 47 | offset = list_end 48 | 49 | return certs 50 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry>=0.12"] 3 | build-backend = "poetry.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "python-opsi" 7 | version = "4.3.10.4" 8 | description = "The opsi python library" 9 | homepage = "https://www.opsi.org" 10 | license = "AGPL-3.0" 11 | maintainers = ["uib GmbH "] 12 | authors = ["uib GmbH "] 13 | include = ["python-opsi_data/**/*"] 14 | [[tool.poetry.packages]] 15 | include = "OPSI" 16 | 17 | [[tool.poetry.source]] 18 | name = "uibpypi" 19 | url = "https://pypi.uib.gmbh/simple" 20 | priority = "primary" 21 | 22 | [[tool.poetry.source]] 23 | name = "PyPI" 24 | priority = "primary" 25 | 26 | [tool.poetry.dependencies] 27 | python = ">=3.11,<3.14" 28 | attrs = ">=24.2" 29 | colorlog = ">=6.6" 30 | ldap3 = ">=2.9" 31 | lz4 = ">=4.0" 32 | msgpack = ">=1.0" 33 | packaging = ">=24.1" 34 | pefile = ">=2022.5" 35 | pexpect = ">=4.8" 36 | psutil = ">=6.0" 37 | pyasn1 = ">=0.6" 38 | pycryptodome = ">=3.10" 39 | python-opsi-common = ">=4.3,<4.4" 40 | python-pam = ">=2.0" 41 | pyzsync = ">=1.2" 42 | ruyaml = ">=0.91" 43 | service-identity = ">=24.1" 44 | six = ">=1.16" 45 | sqlalchemy = ">=1.4,<2.0" 46 | tomlkit = ">=0.13" 47 | typing-extensions = ">=4.12" 48 | cryptography = ">=42.0" 49 | 50 | [tool.ruff.format] 51 | indent-style = "tab" 52 | 53 | [tool.ruff.lint] 54 | ignore = ["F405"] 55 | 56 | [tool.poetry.dependencies.distro] 57 | platform = "linux" 58 | version = ">=1.5" 59 | 60 | [tool.poetry.dependencies.pywin32] 61 | platform = "win32" 62 | version = ">=303" 63 | 64 | [tool.poetry.dependencies.wmi] 65 | platform = "win32" 66 | version = ">=1.5" 67 | 68 | [tool.poetry.group.dev.dependencies] 69 | mock = ">=5.0" 70 | pytest = ">=8.3" 71 | pytest-asyncio = ">=0.24" 72 | pytest-cov = ">=5.0" 73 | ruff = ">=0.7" 74 | -------------------------------------------------------------------------------- /tests/test_util_task_initialize_backend.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing the backend initialisation. 8 | """ 9 | 10 | import OPSI.Util.Task.InitializeBackend as initBackend 11 | 12 | 13 | def testGettingServerConfig(): 14 | networkConfig = { 15 | "ipAddress": "192.168.12.34", 16 | "hardwareAddress": "acabacab", 17 | "subnet": "192.168.12.0", 18 | "netmask": "255.255.255.0", 19 | } 20 | fqdn = "blackwidow.test.invalid" 21 | 22 | config = initBackend._getServerConfig(fqdn, networkConfig) 23 | 24 | assert config["id"] == fqdn 25 | for key in ( 26 | "opsiHostKey", 27 | "description", 28 | "notes", 29 | "inventoryNumber", 30 | "masterDepotId", 31 | ): 32 | assert config[key] is None 33 | 34 | assert config["ipAddress"] == networkConfig["ipAddress"] 35 | assert config["hardwareAddress"] == networkConfig["hardwareAddress"] 36 | assert config["maxBandwidth"] == 0 37 | assert config["isMasterDepot"] is True 38 | assert config["depotLocalUrl"] == "file:///var/lib/opsi/depot" 39 | assert config["depotRemoteUrl"] == f"smb://{fqdn}/opsi_depot" 40 | assert config["depotWebdavUrl"] == f"webdavs://{fqdn}:4447/depot" 41 | assert config["repositoryLocalUrl"] == "file:///var/lib/opsi/repository" 42 | assert config["repositoryRemoteUrl"] == f"webdavs://{fqdn}:4447/repository" 43 | assert config["workbenchLocalUrl"] == "file:///var/lib/opsi/workbench" 44 | assert config["workbenchRemoteUrl"] == f"smb://{fqdn}/opsi_workbench" 45 | assert ( 46 | config["networkAddress"] 47 | == f"{networkConfig['subnet']}/{networkConfig['netmask']}" 48 | ) 49 | -------------------------------------------------------------------------------- /OPSI/Backend/Manager/Authentication/__init__.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Authentication helper. 8 | """ 9 | 10 | from typing import Set 11 | 12 | from opsicommon.logging import get_logger 13 | 14 | from OPSI.Config import OPSI_ADMIN_GROUP 15 | from OPSI.Exceptions import BackendAuthenticationError 16 | from OPSI.Util.File.Opsi import OpsiConfFile 17 | 18 | logger = get_logger("opsi.general") 19 | 20 | 21 | class AuthenticationModule: 22 | def __init__(self): 23 | pass 24 | 25 | def get_instance(self): 26 | return self.__class__() 27 | 28 | def authenticate(self, username: str, password: str) -> None: 29 | raise BackendAuthenticationError("Not implemented") 30 | 31 | def get_groupnames(self, username: str) -> Set[str]: 32 | return set() 33 | 34 | def get_admin_groupname(self) -> str: 35 | return OPSI_ADMIN_GROUP 36 | 37 | def get_read_only_groupnames(self) -> Set[str]: 38 | return set(OpsiConfFile().getOpsiGroups("readonly") or []) 39 | 40 | def user_is_admin(self, username: str) -> bool: 41 | return self.get_admin_groupname() in self.get_groupnames(username) 42 | 43 | def user_is_read_only( 44 | self, username: str, forced_user_groupnames: Set[str] = None 45 | ) -> bool: 46 | user_groupnames = set() 47 | if forced_user_groupnames is None: 48 | user_groupnames = self.get_groupnames(username) 49 | else: 50 | user_groupnames = forced_user_groupnames 51 | 52 | read_only_groupnames = self.get_read_only_groupnames() 53 | for group_name in user_groupnames: 54 | if group_name in read_only_groupnames: 55 | return True 56 | return False 57 | -------------------------------------------------------------------------------- /OPSI/Config.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Various important configuration values. 8 | 9 | This module should be used to refer to often used values in a consistent 10 | way instead of hardcoding the values. 11 | 12 | If new values are added they must be added that the module stays 13 | functional independen of the current underlying system. 14 | 15 | These values are not intended to be changed on-the-fly! 16 | Doing so might result in unforseen problems and is strongly discouraged! 17 | """ 18 | 19 | # Group used to identify members whits administrative rights in opsi 20 | OPSI_ADMIN_GROUP = "opsiadmin" 21 | 22 | # Default user when accessing the opsi depot 23 | DEFAULT_DEPOT_USER = "pcpatch" 24 | 25 | 26 | # Default home dir of depot user 27 | DEFAULT_DEPOT_USER_HOME = "/var/lib/opsi" 28 | 29 | # Path to global opsi configuration file 30 | OPSI_GLOBAL_CONF = "/etc/opsi/global.conf" 31 | 32 | try: 33 | from OPSI.Util.File.Opsi import OpsiConfFile 34 | 35 | OPSI_ADMIN_GROUP = OpsiConfFile().getOpsiAdminGroup() 36 | FILE_ADMIN_GROUP = OpsiConfFile().getOpsiFileAdminGroup() 37 | except Exception: 38 | # Use "pcpatch" if group exists otherwise use the new default "opsifileadmins" 39 | try: 40 | import grp 41 | 42 | grp.getgrnam("pcpatch") 43 | FILE_ADMIN_GROUP = "pcpatch" 44 | except (KeyError, ImportError): 45 | FILE_ADMIN_GROUP = "opsifileadmins" 46 | 47 | # User that is running opsiconfd. 48 | try: 49 | # pyright: reportMissingImports=false 50 | from opsiconfd.config import config 51 | 52 | OPSICONFD_USER = config.run_as_user 53 | except Exception: 54 | OPSICONFD_USER = "opsiconfd" 55 | -------------------------------------------------------------------------------- /tests/Backends/config.py.gitlabci: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of python-opsi. 4 | # Copyright (C) 2013-2019 uib GmbH 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as 8 | # published by the Free Software Foundation, either version 3 of the 9 | # License, or (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | """ 19 | Backend configuration for tests. 20 | 21 | Please adjust this file to your settings and rename it to ``config.py`` 22 | 23 | **DO NOT USE YOUR PRODUCTION SETTINGS HERE** 24 | :author: Niko Wenselowski 25 | :license: GNU Affero General Public License version 3 26 | """ 27 | 28 | # Configuration for the MySQL backend. 29 | # This accepts the same data as can be found in your 30 | # /etc/opsi/backends/mysql.conf but you should not use that database 31 | # for your tests because the tests will delete 32 | MySQLconfiguration = { 33 | "address": "mysql", 34 | "database": "opsi", 35 | "username": "root", 36 | "password": "opsi", 37 | "databaseCharset": "utf8", 38 | "connectionPoolSize": 20, 39 | "connectionPoolMaxOverflow": 10, 40 | "connectionPoolTimeout": 30 41 | } 42 | 43 | # Configuration for the SQLite backend. 44 | # If database is set to :memory: it will use an in-memory database. 45 | # Otherwise you can set the path to an sqlite-db here. 46 | SQLiteconfiguration = { 47 | # "database": "/tmp/opsi.sqlite3", 48 | "database": ":memory:", 49 | "synchronous": True, 50 | "databasecharset": 'utf8' 51 | } 52 | -------------------------------------------------------------------------------- /tests/data/util/task/certificate/example.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBALPsdTWVnLk0VSXz 3 | FBxN+8d4dYpSt6Y3ZtfqV0IscFYM6Q59pQCx1qeNTFr8Rrz0wWc6hE1C18eWnAnl 4 | QNt1hzXthTqN4mvwKTTC+7YJEVWDr/7CyDU/VAvQ0STKWF8WUgnYEigJLPi4gMDu 5 | Fut5o32em9WABM1wg2k2ncDR6McDAgMBAAECgYAQlQJYZemDyCbw0G5SDX3e7GMo 6 | 1GbIkuKPk7FnD+FqjNYN19aVMc6usn8PA6EhWQ1aDjKTTE3Gv0KyRsarczF6xyo3 7 | +a59UxY8ii1EkbV+KpYVjyMW5ggUFmMoqXwc5ra61UlIbYZJx8AX/ma9CnhPO4Za 8 | 8mV4VUTtqh47DrnswQJBAOp/BaBs8PQXjQ3BFczozUdXZygVvopNw/4K9nnT9KMP 9 | Rfb4f0/RRmMkeduEfarW+9LZFTOjvALnr+bQPJBCyf8CQQDEbE3aiTBSYbEvN/dr 10 | ioeN4ekZe5399/Mewib/RsXhTjpic5w6mW/1Ustf7zBiaGiKwtDhjbXDK7Sa6XC4 11 | HNr9AkBftqUXTCA1oX9Dg/JgBw3y9qv2Ypm5XfCHuvXL2EXcYJmQKvHcJHF0eij6 12 | /uNEXie/cjgDMevFy8eykICH6ZsFAkBruE2V7KigdUz7bUD2LDmc2OjB/eYuUp11 13 | H9rNIp4AqqsgNPDXvjAFJQu9p+or/cBfYOm22MV2rHBWiHE1tzVtAkByMezDA/qC 14 | EZ2vGnsov+kN6mpigMNpZHuu/yf+S7usQI/VMwUN7fwvla1SKTn00YjIbk5clI1r 15 | ukeocrRaYQY8 16 | -----END PRIVATE KEY----- 17 | -----BEGIN CERTIFICATE----- 18 | MIICgzCCAeygAwIBAgIJAP+/DMKNHd3YMA0GCSqGSIb3DQEBBQUAMHgxCzAJBgNV 19 | BAYTAkRFMQswCQYDVQQIEwJSUDEOMAwGA1UEBxMFTWFpbnoxDDAKBgNVBAoTA1VJ 20 | QjENMAsGA1UECxMEdGVzdDETMBEGA1UEAxMKbmlrby1saW51eDEaMBgGCSqGSIb3 21 | DQEJARYLaW5mb0B1aWIuZGUwHhcNMTMxMDAxMTIyNDUzWhcNMTYwNjI3MTIyNDUz 22 | WjB4MQswCQYDVQQGEwJERTELMAkGA1UECBMCUlAxDjAMBgNVBAcTBU1haW56MQww 23 | CgYDVQQKEwNVSUIxDTALBgNVBAsTBHRlc3QxEzARBgNVBAMTCm5pa28tbGludXgx 24 | GjAYBgkqhkiG9w0BCQEWC2luZm9AdWliLmRlMIGfMA0GCSqGSIb3DQEBAQUAA4GN 25 | ADCBiQKBgQCz7HU1lZy5NFUl8xQcTfvHeHWKUremN2bX6ldCLHBWDOkOfaUAsdan 26 | jUxa/Ea89MFnOoRNQtfHlpwJ5UDbdYc17YU6jeJr8Ck0wvu2CRFVg6/+wsg1P1QL 27 | 0NEkylhfFlIJ2BIoCSz4uIDA7hbreaN9npvVgATNcINpNp3A0ejHAwIDAQABoxUw 28 | EzARBglghkgBhvhCAQEEBAMCBkAwDQYJKoZIhvcNAQEFBQADgYEAKgUN6IJ+/o3Y 29 | YzztnlN8I3aeH69+hn680LWiDOD3IFR4Bb2yeEG1F/3TiKn5Ppmw7pl+BHutfatK 30 | p/6kCD4NU+m9Fp0hgd+ffbt6EpZa6iuLSLouC+O4bqWhFTvIdYNYt1IQyg88EBM1 31 | jsW5eOUnSQz+hYIXG73XrZWU+r8zRYk= 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /OPSI/Backend/Backend.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Basic backend. 8 | 9 | This holds the basic backend classes. 10 | """ 11 | 12 | import threading 13 | from contextlib import contextmanager 14 | from typing import Any, Callable 15 | 16 | from .Base import ( 17 | Backend, 18 | BackendModificationListener, 19 | ConfigDataBackend, 20 | ExtendedBackend, 21 | ExtendedConfigDataBackend, 22 | ModificationTrackingBackend, 23 | describeInterface, 24 | ) 25 | 26 | __all__ = ( 27 | "describeInterface", 28 | "temporaryBackendOptions", 29 | "DeferredCall", 30 | "Backend", 31 | "ExtendedBackend", 32 | "ConfigDataBackend", 33 | "ExtendedConfigDataBackend", 34 | "ModificationTrackingBackend", 35 | "BackendModificationListener", 36 | ) 37 | 38 | 39 | @contextmanager 40 | def temporaryBackendOptions(backend: Backend, **options) -> None: 41 | oldOptions = backend.backend_getOptions() 42 | try: 43 | backend.backend_setOptions(options) 44 | yield 45 | finally: 46 | backend.backend_setOptions(oldOptions) 47 | 48 | 49 | class DeferredCall: 50 | def __init__(self, callback: Callable = None) -> None: 51 | self.error = None 52 | self.result = None 53 | self.finished = threading.Event() 54 | self.callback = callback 55 | self.callbackArgs = [] 56 | self.callbackKwargs = {} 57 | 58 | def waitForResult(self) -> Any: 59 | self.finished.wait() 60 | if self.error: 61 | raise self.error 62 | return self.result 63 | 64 | def setCallback(self, callback: Callable, *args, **kwargs) -> None: 65 | self.callback = callback 66 | self.callbackArgs = args 67 | self.callbackKwargs = kwargs 68 | 69 | def _gotResult(self) -> None: 70 | self.finished.set() 71 | if self.callback: 72 | self.callback(self, *self.callbackArgs, **self.callbackKwargs) 73 | -------------------------------------------------------------------------------- /tests/Backends/config.py.example: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of python-opsi. 4 | # Copyright (C) 2013-2019 uib GmbH 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as 8 | # published by the Free Software Foundation, either version 3 of the 9 | # License, or (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | """ 19 | Backend configuration for tests. 20 | 21 | Please adjust this file to your settings and rename it to ``config.py`` 22 | 23 | **DO NOT USE YOUR PRODUCTION SETTINGS HERE** 24 | :author: Niko Wenselowski 25 | :license: GNU Affero General Public License version 3 26 | """ 27 | 28 | # Configuration for the MySQL backend. 29 | # This accepts the same data as can be found in your 30 | # /etc/opsi/backends/mysql.conf but you should not use that database 31 | # for your tests because the tests will delete 32 | MySQLconfiguration = { 33 | "address": "localhost", 34 | "database": "opsi", 35 | "username": "opsi", 36 | "password": "someRandomPassw0rd", 37 | "databaseCharset": "utf8", 38 | "connectionPoolSize": 20, 39 | "connectionPoolMaxOverflow": 10, 40 | "connectionPoolTimeout": 30 41 | } 42 | 43 | # Configuration for the SQLite backend. 44 | # If database is set to :memory: it will use an in-memory database. 45 | # Otherwise you can set the path to an sqlite-db here. 46 | SQLiteconfiguration = { 47 | # "database": "/tmp/opsi.sqlite3", 48 | "database": ":memory:", 49 | "synchronous": True, 50 | "databasecharset": 'utf8' 51 | } 52 | -------------------------------------------------------------------------------- /tests/data/package_control_file/control.yml: -------------------------------------------------------------------------------- 1 | Package: 2 | version: '1' 3 | depends: [] 4 | Product: 5 | type: LocalbootProduct 6 | id: dfn_inkscape 7 | name: Inkscape 8 | description: Editor für 2D-Vektorgrafiken im standardisierten SVG-Dateiformat; Import 9 | von Bildern und Vektoren, sowie PDF 10 | advice: 11 | version: 0.92.4 12 | priority: 0 13 | licenseRequired: false 14 | productClasses: [] 15 | setupScript: setup64.opsiscript 16 | uninstallScript: uninstall64.opsiscript 17 | updateScript: 18 | alwaysScript: 19 | onceScript: 20 | customScript: 21 | userLoginScript: 22 | windowsSoftwareIds: [] 23 | ProductProperties: 24 | - type: BoolProductProperty 25 | name: desktop-link 26 | multivalue: false 27 | editable: false 28 | description: Link on Desktop? 29 | values: 30 | - false 31 | - true 32 | default: 33 | - false 34 | - type: UnicodeProductProperty 35 | name: custom-post-install 36 | multivalue: false 37 | editable: false 38 | description: Define filename for include script in custom directory after installation 39 | values: 40 | - none 41 | - post-install.opsiinc 42 | default: 43 | - none 44 | - type: UnicodeProductProperty 45 | name: custom-post-deinstall 46 | multivalue: false 47 | editable: false 48 | description: Define filename for include script in custom directory after deinstallation 49 | values: 50 | - none 51 | - post-deinstall.opsiinc 52 | default: 53 | - none 54 | - type: UnicodeProductProperty 55 | name: silent-option 56 | multivalue: false 57 | editable: false 58 | description: Un/Install MSI silent (/qb!) or very silent (/qn) 59 | values: 60 | - /qb! 61 | - /qn 62 | default: 63 | - /qb! 64 | ProductDependencies: 65 | - required_product_id: dfn_ghostscript 66 | required_product_version: 67 | required_package_version: 68 | action: setup 69 | requirement_type: before 70 | required_action: 71 | required_status: installed 72 | -------------------------------------------------------------------------------- /tests/data/util/task/updatePackages/example_updater.conf: -------------------------------------------------------------------------------- 1 | [general] 2 | ; Where to store package files 3 | packageDir = /var/lib/opsi/repository 4 | ; Location of log file 5 | logFile = /var/log/opsi/opsi-package-updater.log 6 | ; Log level 0...9 7 | logLevel = 5 8 | ; set defaulttimeout 9 | timeout = 60 10 | ; path to temp directory for package installation 11 | tempdir = /tmp 12 | ; directory where the repository configurations are stored 13 | repositoryConfigDir = /etc/opsi/package-updater.repos.d/ 14 | ; proxy to use - can be overridden per repo 15 | proxy = 16 | 17 | [notification] 18 | ; Activate/deactivate eMail notification 19 | active = false 20 | ; SMTP server address 21 | smtphost = smtp 22 | ; SMTP server port 23 | smtpport = 25 24 | ; SMTP username 25 | ;smtpuser = username 26 | ; SMTP password for user 27 | ;smtppassword = s3cR3+ 28 | ; Use STARTTLS 29 | use_starttls = False 30 | ; Sender eMail address 31 | sender = opsi-package-updater@localhost 32 | ; Comma separated list of receivers 33 | receivers = root@localhost, anotheruser@localhost 34 | ; Subject of notification mail 35 | subject = opsi-package-updater example config 36 | 37 | [installation] 38 | ; If window start AND end are set, installation of the newly downloaded packages 39 | ; will only be done if the time when all downloads are completed is inside the time window 40 | ; Times have to be speciefied in the form HH:MM, i.e. 06:30 41 | windowStart = 01:23 42 | windowEnd = 04:56 43 | ; Comma separated list of product ids which will be installed even outside the time window 44 | exceptProductIds = firstProduct, second-product 45 | 46 | [wol] 47 | ; If active is set to true, wake on lan will be sent to clients which need to perform actions 48 | active = false 49 | ; Comma separated list of product ids which will not trigger wake on lan 50 | excludeProductIds = this, that 51 | ; Shutdown clients after installation? 52 | ; Before you set this to true please asure that the product shutdownwanted is installed on the depot 53 | shutdownWanted = true 54 | ; Gap in seconds between wake ups 55 | startGap = 10 56 | -------------------------------------------------------------------------------- /tests/data/util/file/inf_testdata_3.inf: -------------------------------------------------------------------------------- 1 | [Version] 2 | Signature="$WINDOWS NT$" 3 | Class=Processor 4 | ClassGuid={50127DC3-0F36-415e-A6CC-4CB3BE910B65} 5 | Provider=%AMD% 6 | DriverVer=10/26/2004, 1.2.2.0 7 | CatalogFile=AmdK8.cat 8 | 9 | [DestinationDirs] 10 | DefaultDestDir = 12 11 | 12 | [SourceDisksNames] 13 | 1 = %DiskDesc%,,, 14 | 15 | [SourceDisksFiles] 16 | AmdK8.sys = 1 17 | 18 | [ControlFlags] 19 | ; 20 | ; Exclude all devices from Select Device list 21 | ; 22 | ExcludeFromSelect = * 23 | 24 | [ClassInstall32] 25 | AddReg=Processor_Class_Addreg 26 | 27 | [Processor_Class_Addreg] 28 | HKR,,,0,%ProcessorClassName% 29 | HKR,,NoInstallClass,,1 30 | HKR,,Icon,,"-28" 31 | 32 | [Manufacturer] 33 | %AMD%=AmdK8 34 | 35 | [AmdK8] 36 | %AmdK8.DeviceDesc% = AmdK8_Inst,ACPI\AuthenticAMD_-_x86_Family_15 37 | %AmdK8.DeviceDesc% = AmdK8_Inst,ACPI\AuthenticAMD_-_AMD64_Family_15 38 | 39 | [AmdK8_Inst.NT] 40 | Copyfiles = @AmdK8.sys 41 | 42 | [AmdK8_Inst.NT.Services] 43 | AddService = AmdK8,%SPSVCINST_ASSOCSERVICE%,AmdK8_Service_Inst,AmdK8_EventLog_Inst 44 | 45 | [AmdK8_Service_Inst] 46 | DisplayName = %AmdK8.SvcDesc% 47 | ServiceType = %SERVICE_KERNEL_DRIVER% 48 | StartType = %SERVICE_SYSTEM_START% 49 | ErrorControl = %SERVICE_ERROR_NORMAL% 50 | ServiceBinary = %12%\AmdK8.sys 51 | LoadOrderGroup = Extended Base 52 | AddReg = AmdK8_Inst_AddReg 53 | 54 | [AmdK8_Inst_AddReg] 55 | HKR,"Parameters",Capabilities,0x00010001,0x80 56 | 57 | [AmdK8_EventLog_Inst] 58 | AddReg = AmdK8_EventLog_AddReg 59 | 60 | [AmdK8_EventLog_AddReg] 61 | HKR,,EventMessageFile,0x00020000,"%%SystemRoot%%\System32\IoLogMsg.dll;%%SystemRoot%%\System32\drivers\AmdK8.sys" 62 | HKR,,TypesSupported,0x00010001,7 63 | 64 | [strings] 65 | AMD = "Advanced Micro Devices" 66 | ProcessorClassName = "Processors" 67 | AmdK8.DeviceDesc = "AMD K8 Processor" 68 | AmdK8.SvcDesc = "AMD Processor Driver" 69 | DiskDesc = "AMD Processor Driver Disk" 70 | 71 | SPSVCINST_ASSOCSERVICE= 0x00000002 72 | SERVICE_KERNEL_DRIVER = 1 73 | SERVICE_SYSTEM_START = 1 74 | SERVICE_ERROR_NORMAL = 1 75 | -------------------------------------------------------------------------------- /tests/data/util/file/inf_testdata_5.inf: -------------------------------------------------------------------------------- 1 | ; 2 | ; SER2PL.INF (for Windows 2000) 3 | ; 4 | ; Copyright (c) 2000, Prolific Technology Inc. 5 | 6 | [version] 7 | signature="$Windows NT$" 8 | Class=Ports 9 | ClassGuid={4D36E978-E325-11CE-BFC1-08002BE10318} 10 | Provider=%Pro% 11 | catalogfile=pl2303.cat 12 | DriverVer=12/31/2002,2.0.0.7 13 | 14 | [SourceDisksNames] 15 | 1=%Pro.Disk%,,, 16 | 17 | [ControlFlags] 18 | ExcludeFromSelect = USB\VID_067b&PID_2303 19 | 20 | [SourceDisksFiles] 21 | ser2pl.sys=1 22 | 23 | [DestinationDirs] 24 | DefaultDestDir=12 25 | ComPort.NT.Copy=12 26 | 27 | [Manufacturer] 28 | %Pro%=Pro 29 | 30 | [Pro] 31 | %DeviceDesc% = ComPort, USB\VID_067B&PID_2303 32 | 33 | [ComPort.NT] 34 | CopyFiles=ComPort.NT.Copy 35 | AddReg=ComPort.NT.AddReg 36 | 37 | [ComPort.NT.HW] 38 | AddReg=ComPort.NT.HW.AddReg 39 | 40 | [ComPort.NT.Copy] 41 | ser2pl.sys 42 | 43 | [ComPort.NT.AddReg] 44 | HKR,,DevLoader,,*ntkern 45 | HKR,,NTMPDriver,,ser2pl.sys 46 | HKR,,EnumPropPages32,,"MsPorts.dll,SerialPortPropPageProvider" 47 | 48 | [ComPort.NT.HW.AddReg] 49 | HKR,,"UpperFilters",0x00010000,"serenum" 50 | 51 | [ComPort.NT.Services] 52 | AddService = Ser2pl, 0x00000002, Serial_Service_Inst 53 | AddService = Serenum,,Serenum_Service_Inst 54 | 55 | [Serial_Service_Inst] 56 | DisplayName = %Serial.SVCDESC% 57 | ServiceType = 1 ; SERVICE_KERNEL_DRIVER 58 | StartType = 3 ; SERVICE_SYSTEM_START (this driver may do detection) 59 | ErrorControl = 1 ; SERVICE_ERROR_IGNORE 60 | ServiceBinary = %12%\ser2pl.sys 61 | LoadOrderGroup = Base 62 | 63 | [Serenum_Service_Inst] 64 | DisplayName = %Serenum.SVCDESC% 65 | ServiceType = 1 ; SERVICE_KERNEL_DRIVER 66 | StartType = 3 ; SERVICE_DEMAND_START 67 | ErrorControl = 1 ; SERVICE_ERROR_NORMAL 68 | ServiceBinary = %12%\serenum.sys 69 | LoadOrderGroup = PNP Filter 70 | 71 | [linji] 72 | Pro = "Prolific" 73 | Pro.Disk="USB-Serial Cable Diskette" 74 | DeviceDesc = "Prolific USB-to-Serial Comm Port" 75 | Serial.SVCDESC = "Prolific Serial port driver" 76 | Serenum.SVCDESC = "Serenum Filter Driver" -------------------------------------------------------------------------------- /tests/Backends/MySQL.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | MySQL backend test helpers 8 | """ 9 | 10 | from contextlib import contextmanager 11 | 12 | import pytest 13 | 14 | from OPSI.Backend.MySQL import ( 15 | MySQL, 16 | MySQLBackend, 17 | MySQLBackendObjectModificationTracker, 18 | ) 19 | from OPSI.Util.Task.UpdateBackend.MySQL import disableForeignKeyChecks 20 | 21 | try: 22 | from .config import MySQLconfiguration 23 | except ImportError: 24 | MySQLconfiguration = None 25 | 26 | UNKNOWN_TABLE_ERROR_CODE = 1051 27 | 28 | 29 | @contextmanager 30 | def getMySQLBackend(**backendOptions): 31 | if not MySQLconfiguration: 32 | pytest.skip("no MySQL backend configuration given.") 33 | 34 | optionsForBackend = MySQLconfiguration 35 | optionsForBackend.update(backendOptions) 36 | 37 | with cleanDatabase(MySQL(**optionsForBackend)): 38 | yield MySQLBackend(**optionsForBackend) 39 | 40 | 41 | @contextmanager 42 | def getMySQLModificationTracker(): 43 | if not MySQLconfiguration: 44 | pytest.skip("no MySQL backend configuration given.") 45 | 46 | yield MySQLBackendObjectModificationTracker(**MySQLconfiguration) 47 | 48 | 49 | @contextmanager 50 | def cleanDatabase(database): 51 | def dropAllTables(database): 52 | with database.session() as session: 53 | with disableForeignKeyChecks(database, session): 54 | # Drop database 55 | error_count = 0 56 | success = False 57 | while not success: 58 | success = True 59 | for table_name in getTableNames(database, session): 60 | drop_command = f"DROP TABLE `{table_name}`" 61 | try: 62 | database.execute(session, drop_command) 63 | except Exception: 64 | success = False 65 | error_count += 1 66 | if error_count > 10: 67 | raise 68 | 69 | dropAllTables(database) 70 | try: 71 | yield database 72 | finally: 73 | dropAllTables(database) 74 | 75 | 76 | def getTableNames(database, session): 77 | return set(tuple(i.values())[0] for i in database.getSet(session, "SHOW TABLES")) 78 | -------------------------------------------------------------------------------- /tests/test_system_posix_distribution.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing Distribution functionality from OPSI.System.Posix 8 | """ 9 | 10 | from OPSI.System.Posix import Distribution 11 | 12 | import pytest 13 | 14 | 15 | # The first tuple is retrieved by running platform.linux_distribution() 16 | # or distro.linux_distribution() on the corresponding version. 17 | DISTRI_INFOS = [ 18 | (("debian", "8.11", ""), (8, 11)), 19 | (("debian", "10.0", ""), (10, 0)), 20 | # TODO: add CentOS 7 21 | (("Red Hat Enterprise Linux Server", "7.0", "Maipo"), (7, 0)), 22 | (("Ubuntu", "16.04", "xenial"), (16, 4)), 23 | (("Ubuntu", "19.04", "disco"), (19, 4)), 24 | (("Univention", '"4.4-0 errata175"', "Blumenthal"), (4, 4)), 25 | # TODO: add SLES12 26 | (("openSUSE project", "42.3", "n/a"), (42, 3)), 27 | (("openSUSE", "15.0", "n/a"), (15, 0)), 28 | ] 29 | 30 | 31 | @pytest.mark.parametrize("dist_info, expected_version", DISTRI_INFOS) 32 | def testDistributionHasVersionSet(dist_info, expected_version): 33 | dist = Distribution(distribution_information=dist_info) 34 | 35 | assert dist.version 36 | assert expected_version == dist.version 37 | assert isinstance(dist.version, tuple) 38 | 39 | 40 | @pytest.mark.parametrize("dist_info, expected_version", DISTRI_INFOS) 41 | def testDistributionReprContainsAllValues(dist_info, expected_version): 42 | dist = Distribution(distribution_information=dist_info) 43 | 44 | for part in dist_info: 45 | assert part.strip() in repr(dist) 46 | 47 | 48 | @pytest.mark.parametrize("dist_info, expected_version", DISTRI_INFOS) 49 | def testDistributionNameGetsWhitespaceStripped(dist_info, expected_version): 50 | dist = Distribution(distribution_information=dist_info) 51 | 52 | assert dist.distribution == dist_info[0].strip() 53 | 54 | 55 | @pytest.mark.parametrize("dist_info, expected_version", DISTRI_INFOS) 56 | def testDistributionHasDistributorSet(dist_info, expected_version): 57 | dist = Distribution(distribution_information=dist_info) 58 | 59 | assert dist.distributor 60 | -------------------------------------------------------------------------------- /tests/test_backend_jsonrpc.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing jsonrpc backend functionality. 8 | """ 9 | 10 | import json 11 | from pathlib import Path 12 | from typing import Any 13 | 14 | import pytest 15 | from opsicommon.exceptions import OpsiServiceConnectionError 16 | from opsicommon.testing.helpers import http_test_server 17 | 18 | from OPSI.Backend.JSONRPC import JSONRPCBackend 19 | 20 | 21 | def test_jsonrpc_backend(tmp_path: Path) -> None: 22 | log_file = tmp_path / "request.log" 23 | interface: list[dict[str, Any]] = [ 24 | { 25 | "name": "test_method", 26 | "params": ["arg1", "*arg2", "**arg3"], 27 | "args": ["arg1", "arg2"], 28 | "varargs": None, 29 | "keywords": "arg4", 30 | "defaults": ["default2"], 31 | "deprecated": False, 32 | "alternative_method": None, 33 | "doc": None, 34 | "annotations": {}, 35 | }, 36 | { 37 | "name": "backend_getInterface", 38 | "params": [], 39 | "args": ["self"], 40 | "varargs": None, 41 | "keywords": None, 42 | "defaults": None, 43 | "deprecated": False, 44 | "alternative_method": None, 45 | "doc": None, 46 | "annotations": {}, 47 | }, 48 | { 49 | "name": "backend_exit", 50 | "params": [], 51 | "args": ["self"], 52 | "varargs": None, 53 | "keywords": None, 54 | "defaults": None, 55 | "deprecated": False, 56 | "alternative_method": None, 57 | "doc": None, 58 | "annotations": {}, 59 | }, 60 | ] 61 | with http_test_server( 62 | generate_cert=True, 63 | log_file=log_file, 64 | response_headers={"server": "opsiconfd 4.3.0.0 (uvicorn)"}, 65 | ) as server: 66 | server.response_body = json.dumps( 67 | {"jsonrpc": "2.0", "result": interface} 68 | ).encode("utf-8") 69 | server.response_headers["Content-Type"] = "application/json" 70 | backend = JSONRPCBackend(address=f"https://localhost:{server.port}") 71 | backend.test_method("arg1") 72 | 73 | with pytest.raises(OpsiServiceConnectionError): 74 | backend = JSONRPCBackend(address=f"https://localhost:{server.port+1}") 75 | -------------------------------------------------------------------------------- /tests/data/package_control_file/changelog.txt: -------------------------------------------------------------------------------- 1 | dfn_inkscape (0.92.4-1) 2 | * neue Upstreamversion (http://wiki.inkscape.org/wiki/index.php/Release_notes/0.92.4) 3 | -- Thomas Besser (archIT/KIT) , 21.01.2019 4 | 5 | dfn_inkscape (0.92.3-2) 6 | * neues o4i-Logo 7 | * neue Registrysuche (https://github.com/opsi4instituts/lib, winst-Version 4.12.0.16 Voraussetzung) 8 | * Verwendung uib_exitcode (local function) 9 | * Check Version (Paket <-> Installation) 10 | -- David Dams (archIT/KIT) , 07.01.2019 11 | 12 | dfn_inkscape (0.92.3-1) 13 | * neue Upstreamversion (http://wiki.inkscape.org/wiki/index.php/Release_notes/0.92.3) 14 | * o4i-Kosmetik (desktoplink -> desktop-link, msi-silent-option -> silent-option) 15 | -- Thomas Besser (archIT/KIT) , 26.03.2018 16 | 17 | dfn_inkscape (0.92.2-1) 18 | * neue Upstreamversion (stability and bugfix release) 19 | * alte uib Copyrights (Überbleibsel von opsi-template) entfernt 20 | * Desktopicon -> Desktoplink gem. o4i-Richtlinie angepasst 21 | * o4i-Logo: Anzeigeaufruf nach common.opsiinc ausgelagert, eigenes Logo möglich 22 | -- Thomas Besser (archIT/KIT) , 17.02.2017 23 | 24 | dfn_inkscape (0.92.1-1) 25 | * neue Upstreamversion (stability and bugfix release) 26 | * Minor-Versionsnummer via $InstFile$ 27 | -- Thomas Besser (archIT/KIT) , 17.02.2017 28 | 29 | dfn_inkscape (0.92-1) 30 | * o4i-Logo, MSI-Check-Exitcode, ProductProperty MSISilentOption hinzugefügt 31 | * Check auf 64-Bit-System bzw. Win-Version nach common.opsiinc 32 | * ProductProperty install_architecture entfernt, da nur 64-Bit im Paket 33 | * Version aus Paket holen für $InstFile$ 34 | -- Thomas Besser (archIT/KIT) , 09.01.2017 35 | 36 | dfn_inkscape (0.91-2) 37 | * Copy&Paste-Überbleibsel entfernt ;-) 38 | * Bugfix "InstallLocation" bzw. "DisplayIcon" an die richtige Stelle in Registry schreiben 39 | * Icon hinzugefügt 40 | -- Thomas Besser (archIT/KIT) , 13.08.2015 41 | 42 | dfn_inkscape (0.91-1) 43 | * initiales DFN-Paket 44 | * angepasstes MSI-Paket, das kein Desktopicon anlegt 45 | * MSI speichert 'InstallLocation' nicht ab bzw. 'DisplayIcon' fehlt -> manuell in Registry schreiben 46 | -- Thomas Besser (archIT/KIT) , 12.08.2015 -------------------------------------------------------------------------------- /gettext/python-opsi.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # FIRST AUTHOR , YEAR. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "POT-Creation-Date: 2016-08-24 17:11+CEST\n" 9 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "Last-Translator: FULL NAME \n" 11 | "Language-Team: LANGUAGE \n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=CHARSET\n" 14 | "Content-Transfer-Encoding: ENCODING\n" 15 | "Generated-By: pygettext.py 1.5\n" 16 | 17 | 18 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:117 ../OPSI/UI.py:138 ../OPSI/UI.py:141 19 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:147 ../OPSI/UI.py:244 ../OPSI/UI.py:283 20 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:579 ../OPSI/UI.py:679 21 | msgid "OK" 22 | msgstr "" 23 | 24 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:244 25 | msgid "An error occurred" 26 | msgstr "" 27 | 28 | #: ../OPSI/UI.py:117 ../OPSI/UI.py:283 29 | msgid "Message" 30 | msgstr "" 31 | 32 | #: ../OPSI/UI.py:120 ../OPSI/UI.py:126 ../OPSI/UI.py:322 ../OPSI/UI.py:352 33 | msgid "Progress" 34 | msgstr "" 35 | 36 | #: ../OPSI/UI.py:123 ../OPSI/UI.py:129 ../OPSI/UI.py:337 ../OPSI/UI.py:367 37 | msgid "Copy progress" 38 | msgstr "" 39 | 40 | #: ../OPSI/UI.py:132 ../OPSI/UI.py:382 41 | msgid "Text" 42 | msgstr "" 43 | 44 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:141 ../OPSI/UI.py:144 ../OPSI/UI.py:147 45 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:579 ../OPSI/UI.py:679 46 | msgid "Cancel" 47 | msgstr "" 48 | 49 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:396 50 | msgid "Please type text" 51 | msgstr "" 52 | 53 | #: ../OPSI/UI.py:141 ../OPSI/UI.py:472 54 | msgid "Please select" 55 | msgstr "" 56 | 57 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:579 58 | msgid "Please fill in" 59 | msgstr "" 60 | 61 | #: ../OPSI/UI.py:147 ../OPSI/UI.py:679 62 | msgid "Question" 63 | msgstr "" 64 | 65 | #: ../OPSI/UI.py:152 ../OPSI/UI.py:169 ../OPSI/UI.py:184 ../OPSI/UI.py:733 66 | #: ../OPSI/UI.py:833 ../OPSI/UI.py:898 67 | msgid "Title" 68 | msgstr "" 69 | 70 | #: ../OPSI/UI.py:275 ../OPSI/UI.py:314 71 | msgid " %s | select | scroll text" 72 | msgstr "" 73 | 74 | #: ../OPSI/UI.py:451 ../OPSI/UI.py:552 ../OPSI/UI.py:651 ../OPSI/UI.py:711 75 | msgid " %s | %s | move cursor | select" 76 | msgstr "" 77 | 78 | #: ../OPSI/UI.py:453 ../OPSI/UI.py:554 ../OPSI/UI.py:653 ../OPSI/UI.py:713 79 | msgid " | scroll text" 80 | msgstr "" 81 | 82 | -------------------------------------------------------------------------------- /tests/test_util_product.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing the OPSI.Util.Product module. 8 | """ 9 | 10 | import os 11 | import re 12 | import tempfile 13 | 14 | import pytest 15 | 16 | import OPSI.Util.Product as Product 17 | 18 | from .helpers import cd, mock 19 | 20 | 21 | @pytest.mark.parametrize( 22 | "text", 23 | [ 24 | ".svn", 25 | pytest.param(".svnotmatching", marks=pytest.mark.xfail), 26 | ".git", 27 | pytest.param(".gitignore", marks=pytest.mark.xfail), 28 | ], 29 | ) 30 | def testDirectoryExclusion(text): 31 | assert re.match(Product.EXCLUDE_DIRS_ON_PACK_REGEX, text) is not None 32 | 33 | 34 | def testProductPackageFileRemovingFolderWithUnicodeFilenamesInsideFails(tempDir): 35 | """ 36 | As mentioned in http://bugs.python.org/issue3616 the attempt to 37 | remove a filename that contains unicode can fail. 38 | 39 | Sometimes products are created that feature files with filenames 40 | that do containt just that. 41 | We need to make shure that removing such fails does not fail and 42 | that we are able to remove them. 43 | """ 44 | tempPackageFilename = tempfile.NamedTemporaryFile(suffix=".opsi") 45 | 46 | ppf = Product.ProductPackageFile(tempPackageFilename.name) 47 | ppf.setClientDataDir(tempDir) 48 | 49 | fakeProduct = mock.Mock() 50 | fakeProduct.getId.return_value = "umlauts" 51 | fakePackageControlFile = mock.Mock() 52 | fakePackageControlFile.getProduct.return_value = fakeProduct 53 | 54 | # Setting up evil file 55 | targetDir = os.path.join(tempDir, "umlauts") 56 | os.makedirs(targetDir) 57 | 58 | with cd(targetDir): 59 | os.system(r"touch -- $(echo -e '--\0250--')") 60 | 61 | with mock.patch.object(ppf, "packageControlFile", fakePackageControlFile): 62 | ppf.deleteProductClientDataDir() 63 | 64 | assert not os.path.exists( 65 | targetDir 66 | ), "Product directory in depot should be deleted." 67 | 68 | 69 | def testSettigUpProductPackageFileWithNonExistingFileFails(): 70 | with pytest.raises(Exception): 71 | Product.ProductPackageFile("nonexisting.opsi") 72 | 73 | 74 | def testCreatingProductPackageSourceRequiresExistingSourceFolder(tempDir): 75 | targetDir = os.path.join(tempDir, "nope") 76 | 77 | with pytest.raises(Exception): 78 | Product.ProductPackageSource(targetDir) 79 | -------------------------------------------------------------------------------- /tests/data/util/file/txtsetupoem_testdata_3.oem: -------------------------------------------------------------------------------- 1 | [Disks] 2 | d1 = "NVIDIA AHCI DRIVER (SCSI)",\disk1,\ 3 | 4 | [Defaults] 5 | 6 | [scsi] 7 | BUSDRV = "NVIDIA nForce Storage Controller (required)" 8 | 9 | [Files.scsi.BUSDRV] 10 | driver = d1,nvgts.sys,BUSDRV 11 | inf = d1, nvgts.inf 12 | catalog = d1, nvata.cat 13 | dll = d1,nvraidco.dll 14 | dll = d1,NvRCoENU.dll 15 | dll = d1,NvRCoAr.dll 16 | dll = d1,NvRCoCs.dll 17 | dll = d1,NvRCoDa.dll 18 | dll = d1,NvRCoDe.dll 19 | dll = d1,NvRCoEl.dll 20 | dll = d1,NvRCoEng.dll 21 | dll = d1,NvRCoEs.dll 22 | dll = d1,NvRCoEsm.dll 23 | dll = d1,NvRCoFi.dll 24 | dll = d1,NvRCoFr.dll 25 | dll = d1,NvRCoHe.dll 26 | dll = d1,NvRCoHu.dll 27 | dll = d1,NvRCoIt.dll 28 | dll = d1,NvRCoJa.dll 29 | dll = d1,NvRCoKo.dll 30 | dll = d1,NvRCoNl.dll 31 | dll = d1,NvRCoNo.dll 32 | dll = d1,NvRCoPl.dll 33 | dll = d1,NvRCoPt.dll 34 | dll = d1,NvRCoPtb.dll 35 | dll = d1,NvRCoRu.dll 36 | dll = d1,NvRCoSk.dll 37 | dll = d1,NvRCoSl.dll 38 | dll = d1,NvRCoSv.dll 39 | dll = d1,NvRCoTh.dll 40 | dll = d1,NvRCoTr.dll 41 | dll = d1,NvRCoZhc.dll 42 | dll = d1,NvRCoZht.dll 43 | 44 | [Config.BUSDRV] 45 | value = parameters\PnpInterface,5,REG_DWORD,1 46 | 47 | [HardwareIds.scsi.BUSDRV] 48 | id = "PCI\VEN_10DE&DEV_0036", "nvgts" 49 | id = "PCI\VEN_10DE&DEV_003E", "nvgts" 50 | id = "PCI\VEN_10DE&DEV_0054", "nvgts" 51 | id = "PCI\VEN_10DE&DEV_0055", "nvgts" 52 | id = "PCI\VEN_10DE&DEV_0266", "nvgts" 53 | id = "PCI\VEN_10DE&DEV_0267", "nvgts" 54 | id = "PCI\VEN_10DE&DEV_037E", "nvgts" 55 | id = "PCI\VEN_10DE&DEV_037F", "nvgts" 56 | id = "PCI\VEN_10DE&DEV_036F", "nvgts" 57 | id = "PCI\VEN_10DE&DEV_03F6", "nvgts" 58 | id = "PCI\VEN_10DE&DEV_03F7", "nvgts" 59 | id = "PCI\VEN_10DE&DEV_03E7", "nvgts" 60 | id = "PCI\VEN_10DE&DEV_044D", "nvgts" 61 | id = "PCI\VEN_10DE&DEV_044E", "nvgts" 62 | id = "PCI\VEN_10DE&DEV_044F", "nvgts" 63 | id = "PCI\VEN_10DE&DEV_0554", "nvgts" 64 | id = "PCI\VEN_10DE&DEV_0555", "nvgts" 65 | id = "PCI\VEN_10DE&DEV_0556", "nvgts" 66 | id = "PCI\VEN_10DE&DEV_07F4", "nvgts" 67 | id = "PCI\VEN_10DE&DEV_07F5", "nvgts" 68 | id = "PCI\VEN_10DE&DEV_07F6", "nvgts" 69 | id = "PCI\VEN_10DE&DEV_07F7", "nvgts" 70 | id = "PCI\VEN_10DE&DEV_0768", "nvgts" 71 | id = "PCI\VEN_10DE&DEV_0AD5", "nvgts" 72 | id = "PCI\VEN_10DE&DEV_0AD4", "nvgts" 73 | id = "PCI\VEN_10DE&DEV_0AB9", "nvgts" 74 | id = "PCI\VEN_10DE&DEV_0AB8", "nvgts" 75 | id = "PCI\VEN_10DE&DEV_0BCC", "nvgts" 76 | id = "PCI\VEN_10DE&DEV_0BCD", "nvgts" -------------------------------------------------------------------------------- /tests/data/backend/small_hwaudit.conf: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | global OPSI_HARDWARE_CLASSES 4 | OPSI_HARDWARE_CLASSES = [ 5 | { 6 | "Class": { 7 | "Type": "VIRTUAL", 8 | "Opsi": "BASIC_INFO" 9 | }, 10 | "Values": [ 11 | { 12 | "Type": "varchar(100)", 13 | "Scope": "g", 14 | "Opsi": "name", 15 | "WMI": "Name", 16 | "Linux": "product" 17 | }, 18 | { 19 | "Type": "varchar(100)", 20 | "Scope": "g", 21 | "Opsi": "description", 22 | "WMI": "Description", 23 | "Linux": "description" 24 | }, 25 | #{ 26 | # "Type": "varchar(60)", 27 | # "Scope": "g", 28 | # "Opsi": "caption", 29 | # "WMI": "Caption" 30 | #} 31 | ] 32 | }, 33 | { 34 | "Class": { 35 | "Type": "VIRTUAL", 36 | "Super": [ "BASIC_INFO" ], 37 | "Opsi": "HARDWARE_DEVICE" 38 | }, 39 | "Values": [ 40 | { 41 | "Type": "varchar(50)", 42 | "Scope": "g", 43 | "Opsi": "vendor", 44 | "WMI": "Manufacturer", 45 | "Linux": "vendor" 46 | }, 47 | { 48 | "Type": "varchar(100)", 49 | "Scope": "g", 50 | "Opsi": "model", 51 | "WMI": "Model", 52 | "Linux": "product" 53 | }, 54 | { 55 | "Type": "varchar(50)", 56 | "Scope": "i", 57 | "Opsi": "serialNumber", 58 | "WMI": "SerialNumber", 59 | "Linux": "serial" 60 | }, 61 | ] 62 | }, 63 | { 64 | "Class": { 65 | "Type": "STRUCTURAL", 66 | "Super": [ "HARDWARE_DEVICE" ], 67 | "Opsi": "COMPUTER_SYSTEM", 68 | "WMI": "select * from Win32_ComputerSystem", 69 | "Linux": "[lshw]system" 70 | }, 71 | "Values": [ 72 | { 73 | "Type": "varchar(100)", 74 | "Scope": "i", 75 | "Opsi": "name", 76 | "WMI": "Name", 77 | "Linux": "id" 78 | }, 79 | { 80 | "Type": "varchar(50)", 81 | "Scope": "i", 82 | "Opsi": "systemType", 83 | "WMI": "SystemType", 84 | "Linux": "configuration/chassis" 85 | } 86 | ] 87 | }, 88 | ] 89 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: docker.uib.gmbh/opsi/dev/pybuilder:deb9-py3.13 2 | 3 | stages: 4 | - test 5 | - doc 6 | - publish 7 | 8 | lint-and-test: 9 | #when: manual 10 | services: 11 | - name: mysql:8.3 12 | command: 13 | - --max_connections=1000 14 | - --default-authentication-plugin=mysql_native_password 15 | variables: 16 | MYSQL_ROOT_PASSWORD: "opsi" 17 | MYSQL_DATABASE: "opsi" 18 | OPSI_HOST_ID: "server.opsi.test" 19 | stage: test 20 | script: | 21 | mkdir -p /etc/opsi/licenses 22 | mkdir -p /var/log/opsi 23 | 24 | # Installing opsi test license 25 | [ -z "${OPSILICSRV_TOKEN}" ] && (echo "OPSILICSRV_TOKEN not set" 1>&2 ; exit 1) 26 | wget --header="Authorization: Bearer ${OPSILICSRV_TOKEN}" "https://opsi-license-server.uib.gmbh/api/v1/licenses/test?usage=python-opsi-gitlab-ci" -O /etc/opsi/licenses/test.opsilic 27 | 28 | cp tests/Backends/config.py.gitlabci tests/Backends/config.py 29 | apt-get update 30 | apt-get --yes install default-libmysqlclient-dev librsync1 31 | poetry env use 3.13 32 | poetry lock 33 | poetry install 34 | poetry run ruff check OPSI tests 35 | poetry add mysqlclient==2.1.1 36 | poetry run pytest --tb=short -x -o junit_family=xunit2 --junitxml=testreport.xml --cov-append --cov-report term --cov-report xml -v tests 37 | coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+)%/' 38 | artifacts: 39 | name: 'python-opsi_test' 40 | paths: 41 | - coverage.xml 42 | - testreport.xml 43 | reports: 44 | junit: testreport.xml 45 | expire_in: 14 days 46 | 47 | 48 | apidoc: 49 | stage: doc 50 | when: manual 51 | before_script: 52 | - 'which ssh-agent || (apt update && apt -y install openssh-client)' 53 | - 'which rsync || (apt update && apt -y install rsync)' 54 | - mkdir -p ~/.ssh 55 | - eval $(ssh-agent -s) 56 | - ssh-add <(echo "$BLOG_PUBLISH_PRIVATE_KEY") 57 | script: 58 | - poetry env use 3.13 59 | - poetry lock 60 | - poetry install 61 | - poetry run opsi-dev-cli apidoc makehtml --output python-opsi 62 | - ssh -o StrictHostKeyChecking=no "root@docker1.ext.uib.gmbh" "mkdir -p /var/lib/docker/volumes/docs_nginx_data/_data/python-docs" 63 | - rsync -e "ssh -o StrictHostKeyChecking=no" --delete -azv python-opsi "root@docker1.ext.uib.gmbh:/var/lib/docker/volumes/docs_nginx_data/_data/python-docs/" 64 | 65 | 66 | uibpypi: 67 | stage: publish 68 | script: 69 | - poetry env use 3.13 70 | - poetry lock 71 | - poetry install 72 | - poetry run opsi-dev-tool -l info --uib-pypi-publish 73 | only: 74 | - tags 75 | -------------------------------------------------------------------------------- /data/backendManager/dispatch.conf: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # = = = = = = = = = = = = = = = = = = = = = = = 3 | # = backend dispatch configuration = 4 | # = = = = = = = = = = = = = = = = = = = = = = = 5 | # 6 | # This file configures which methods are dispatched to which backends. 7 | # Entries has to follow the form: 8 | # : 9 | # 10 | # Backend names have to match a backend configuraton file basename .conf beneath /etc/opsi/backends. 11 | # For every method executed on backend dispatcher the first matching regular expression will be decisive. 12 | # 13 | # Typical configurations: 14 | # mysql, opsipxeconfd and dhcpd backend: 15 | # backend_.* : mysql, opsipxeconfd, dhcpd 16 | # host_.* : mysql, opsipxeconfd, dhcpd 17 | # productOnClient_.* : mysql, opsipxeconfd 18 | # configState_.* : mysql, opsipxeconfd 19 | # .* : mysql 20 | # 21 | # 22 | # file, opsipxeconfd and dhcpd backend: 23 | # backend_.* : file, opsipxeconfd, dhcpd 24 | # host_.* : file, opsipxeconfd, dhcpd 25 | # productOnClient_.* : file, opsipxeconfd 26 | # configState_.* : file, opsipxeconfd 27 | # .* : file 28 | # 29 | # 30 | # file and opsipxeconfd dhcpd backend (in case of ext. dhcp) 31 | # backend_.* : file, opsipxeconfd 32 | # host_.* : file, opsipxeconfd 33 | # productOnClient_.* : file, opsipxeconfd 34 | # configState_.* : file, opsipxeconfd 35 | # .* : file 36 | # 37 | # 38 | # Typical configuration on a depot server. 39 | # jsonrpc, opsipxeconfd and dhcpd backend: 40 | # backend_.* : jsonrpc, opsipxeconfd, dhcpd 41 | # .* : jsonrpc 42 | # 43 | # 44 | # file as main backend, mysql as hw/sw invent and license management backend, opsipxeconfd and dhcpd backend: 45 | # backend_.* : file, mysql, opsipxeconfd, dhcpd 46 | # host_.* : file, opsipxeconfd, dhcpd 47 | # productOnClient_.* : file, opsipxeconfd 48 | # configState_.* : file, opsipxeconfd 49 | # license.* : mysql 50 | # softwareLicense.* : mysql 51 | # audit.* : mysql 52 | # .* : file 53 | # 54 | 55 | backend_.* : file, mysql, opsipxeconfd 56 | host_.* : file, opsipxeconfd 57 | productOnClient_.* : file, opsipxeconfd 58 | configState_.* : file, opsipxeconfd 59 | license.* : mysql 60 | softwareLicense.* : mysql 61 | audit.* : mysql 62 | .* : file 63 | -------------------------------------------------------------------------------- /tests/test_util_task_update_backend_data.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing the update of configuration data. 8 | """ 9 | 10 | import pytest 11 | 12 | from OPSI.Object import OpsiClient, OpsiDepotserver, OpsiConfigserver 13 | from OPSI.Util.Task.UpdateBackend.ConfigurationData import updateBackendData 14 | 15 | from .test_hosts import getLocalHostFqdn 16 | from .helpers import mock 17 | 18 | 19 | @pytest.mark.parametrize( 20 | "onSuse, expectedLocalPath", 21 | ( 22 | (True, "file:///var/lib/opsi/workbench"), 23 | (False, "file:///home/opsiproducts"), 24 | ), 25 | ) 26 | def testUpdateBackendData(backendManager, onSuse, expectedLocalPath): 27 | def getDepotAddress(address): 28 | _, addressAndPath = address.split(":") 29 | return addressAndPath.split("/")[2] 30 | 31 | addServers(backendManager) 32 | with mock.patch( 33 | "OPSI.Util.Task.UpdateBackend.ConfigurationData.isOpenSUSE", lambda: onSuse 34 | ): 35 | with mock.patch( 36 | "OPSI.Util.Task.UpdateBackend.ConfigurationData.isSLES", lambda: onSuse 37 | ): 38 | updateBackendData(backendManager) 39 | 40 | servers = backendManager.host_getObjects( 41 | type=["OpsiDepotserver", "OpsiConfigserver"] 42 | ) 43 | assert servers, "No servers found in backend." 44 | 45 | for server in servers: 46 | assert server.workbenchLocalUrl == expectedLocalPath 47 | 48 | depotAddress = getDepotAddress(server.depotRemoteUrl) 49 | expectedAddress = "smb://" + depotAddress + "/opsi_workbench" 50 | assert expectedAddress == server.workbenchRemoteUrl 51 | 52 | 53 | def addServers(backend): 54 | localHostFqdn = getLocalHostFqdn() 55 | configServer = OpsiConfigserver( 56 | id=localHostFqdn, depotRemoteUrl="smb://192.168.123.1/opsi_depot" 57 | ) 58 | backend.host_createObjects([configServer]) 59 | 60 | _, domain = localHostFqdn.split(".", 1) 61 | 62 | def getDepotRemoteUrl(index): 63 | if index % 2 == 0: 64 | return "smb://192.168.123.{}/opsi_depot".format(index) 65 | else: 66 | return "smb://somename/opsi_depot" 67 | 68 | depots = [ 69 | OpsiDepotserver( 70 | id="depot{n}.{domain}".format(n=index, domain=domain), 71 | depotRemoteUrl=getDepotRemoteUrl(index), 72 | ) 73 | for index in range(10) 74 | ] 75 | backend.host_createObjects(depots) 76 | 77 | clients = [ 78 | OpsiClient(id="client{n}.{domain}".format(n=index, domain=domain)) 79 | for index in range(10) 80 | ] 81 | backend.host_createObjects(clients) 82 | -------------------------------------------------------------------------------- /tests/test_util_task_cleanup_backend.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing backend cleaning. 8 | """ 9 | 10 | from OPSI.Object import LocalbootProduct, ProductOnDepot 11 | from OPSI.Util.Task.CleanupBackend import cleanupBackend, cleanUpProducts 12 | 13 | from .test_backend_replicator import ( 14 | checkIfBackendIsFilled, 15 | fillBackend, 16 | fillBackendWithHosts, 17 | ) 18 | 19 | 20 | def testCleanupBackend(cleanableDataBackend): 21 | # TODO: we need checks to see what get's removed and what not. 22 | # TODO: we also should provide some senseless data that will be removed! 23 | fillBackend(cleanableDataBackend) 24 | 25 | cleanupBackend(cleanableDataBackend) 26 | checkIfBackendIsFilled(cleanableDataBackend) 27 | 28 | 29 | def testCleaninUpProducts(cleanableDataBackend): 30 | productIdToClean = "dissection" 31 | 32 | prod1 = LocalbootProduct(productIdToClean, 1, 1) 33 | prod12 = LocalbootProduct(productIdToClean, 1, 2) 34 | prod13 = LocalbootProduct(productIdToClean, 1, 3) 35 | prod2 = LocalbootProduct(productIdToClean + "2", 2, 1) 36 | prod3 = LocalbootProduct("unhallowed", 3, 1) 37 | prod32 = LocalbootProduct("unhallowed", 3, 2) 38 | 39 | products = [prod1, prod12, prod13, prod2, prod3, prod32] 40 | for p in products: 41 | cleanableDataBackend.product_insertObject(p) 42 | 43 | configServer, depotServer, _ = fillBackendWithHosts(cleanableDataBackend) 44 | depot = depotServer[0] 45 | 46 | pod1 = ProductOnDepot( 47 | prod13.id, 48 | prod13.getType(), 49 | prod13.productVersion, 50 | prod13.packageVersion, 51 | configServer.id, 52 | ) 53 | pod1d = ProductOnDepot( 54 | prod13.id, 55 | prod13.getType(), 56 | prod13.productVersion, 57 | prod13.packageVersion, 58 | depot.id, 59 | ) 60 | pod2 = ProductOnDepot( 61 | prod2.id, prod2.getType(), prod2.productVersion, prod2.packageVersion, depot.id 62 | ) 63 | pod3 = ProductOnDepot( 64 | prod3.id, prod3.getType(), prod3.productVersion, prod3.packageVersion, depot.id 65 | ) 66 | 67 | for pod in [pod1, pod1d, pod2, pod3]: 68 | cleanableDataBackend.productOnDepot_insertObject(pod) 69 | 70 | cleanUpProducts(cleanableDataBackend) 71 | 72 | products = cleanableDataBackend.product_getObjects(id=productIdToClean) 73 | assert len(products) == 1 74 | 75 | product = products[0] 76 | assert product.id == productIdToClean 77 | 78 | allProducts = cleanableDataBackend.product_getObjects() 79 | assert len(allProducts) == 3 # prod13, prod2, prod3 80 | -------------------------------------------------------------------------------- /OPSI/Util/Task/UpdateBackend/ConfigurationData.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Updating backend data. 8 | 9 | This holds backend-independent migrations. 10 | """ 11 | 12 | from opsicommon.logging import get_logger 13 | 14 | from OPSI.System.Posix import isOpenSUSE, isSLES 15 | 16 | __all__ = ("updateBackendData",) 17 | 18 | logger = get_logger("opsi.general") 19 | 20 | 21 | def updateBackendData(backend): 22 | setDefaultWorkbenchLocation(backend) 23 | 24 | 25 | def setDefaultWorkbenchLocation(backend): 26 | """ 27 | Set the possibly missing workbench location on the server. 28 | 29 | The value is regarded as missing if it is not set to None. 30 | `workbenchLocalUrl` will be set to `file:///var/lib/opsi/workbench` 31 | on SUSE system and to `file:///home/opsiproducts` on others. 32 | `workbenchRemoteUrl` will use the same value for the depot address 33 | that is set in `depotRemoteUrl` and then will point to the samba 34 | share _opsi_workbench_. 35 | """ 36 | servers = backend.host_getObjects(type=["OpsiDepotserver", "OpsiConfigserver"]) 37 | 38 | if isSLES() or isOpenSUSE(): 39 | # On Suse 40 | localWorkbenchPath = "file:///var/lib/opsi/workbench" 41 | else: 42 | # On non-SUSE systems the path was usually /home/opsiproducts 43 | localWorkbenchPath = "file:///home/opsiproducts" 44 | 45 | changedServers = set() 46 | for server in servers: 47 | if server.getWorkbenchLocalUrl() is None: 48 | logger.notice( 49 | "Setting missing value for workbenchLocalUrl on %s to %s", 50 | server.id, 51 | localWorkbenchPath, 52 | ) 53 | server.setWorkbenchLocalUrl(localWorkbenchPath) 54 | changedServers.add(server) 55 | 56 | if server.getWorkbenchRemoteUrl() is None: 57 | depotAddress = getServerAddress(server.depotRemoteUrl) 58 | remoteWorkbenchPath = f"smb://{depotAddress}/opsi_workbench" 59 | logger.notice( 60 | "Setting missing value for workbenchRemoteUrl on %s to %s", 61 | server.id, 62 | remoteWorkbenchPath, 63 | ) 64 | server.setWorkbenchRemoteUrl(remoteWorkbenchPath) 65 | changedServers.add(server) 66 | 67 | if changedServers: 68 | backend.host_updateObjects(changedServers) 69 | 70 | 71 | def getServerAddress(depotRemoteUrl): 72 | """ 73 | Get the address of the server from the `depotRemoteUrl`. 74 | 75 | :param depotRemoteUrl: the depotRemoteUrl of an OpsiDepotserver 76 | :type depotRemoteUrl: str 77 | :rtype: str 78 | """ 79 | _, addressAndPath = depotRemoteUrl.split(":") 80 | return addressAndPath.split("/")[2] 81 | -------------------------------------------------------------------------------- /OPSI/Backend/Base/ModificationTracking.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Backend that tracks modifications. 8 | """ 9 | 10 | from opsicommon.logging import get_logger 11 | 12 | from .Extended import ExtendedBackend 13 | 14 | __all__ = ("ModificationTrackingBackend", "BackendModificationListener") 15 | 16 | 17 | logger = get_logger("opsi.general") 18 | 19 | 20 | class ModificationTrackingBackend(ExtendedBackend): 21 | def __init__(self, backend, overwrite=True): 22 | ExtendedBackend.__init__(self, backend, overwrite=overwrite) 23 | self._createInstanceMethods() 24 | self._backendChangeListeners = [] 25 | 26 | def addBackendChangeListener(self, backendChangeListener): 27 | if backendChangeListener in self._backendChangeListeners: 28 | return 29 | self._backendChangeListeners.append(backendChangeListener) 30 | 31 | def removeBackendChangeListener(self, backendChangeListener): 32 | if backendChangeListener not in self._backendChangeListeners: 33 | return 34 | self._backendChangeListeners.remove(backendChangeListener) 35 | 36 | def _fireEvent(self, event, *args): 37 | for bcl in self._backendChangeListeners: 38 | try: 39 | meth = getattr(bcl, event) 40 | meth(self, *args) 41 | except Exception as err: 42 | logger.error(err) 43 | 44 | def _executeMethod(self, methodName, **kwargs): 45 | logger.debug( 46 | "ModificationTrackingBackend %s: executing %s on backend %s", 47 | self, 48 | methodName, 49 | self._backend, 50 | ) 51 | meth = getattr(self._backend, methodName) 52 | result = meth(**kwargs) 53 | action = None 54 | if "_" in methodName: 55 | action = methodName.split("_", 1)[1] 56 | 57 | if action in ("insertObject", "updateObject", "deleteObjects"): 58 | value = list(kwargs.values())[0] 59 | if action == "insertObject": 60 | self._fireEvent("objectInserted", value) 61 | elif action == "updateObject": 62 | self._fireEvent("objectUpdated", value) 63 | elif action == "deleteObjects": 64 | self._fireEvent("objectsDeleted", value) 65 | self._fireEvent("backendModified") 66 | 67 | return result 68 | 69 | 70 | class BackendModificationListener: 71 | def objectInserted(self, backend, obj): 72 | # Should return immediately! 73 | pass 74 | 75 | def objectUpdated(self, backend, obj): 76 | # Should return immediately! 77 | pass 78 | 79 | def objectsDeleted(self, backend, objs): 80 | # Should return immediately! 81 | pass 82 | 83 | def backendModified(self, backend): 84 | # Should return immediately! 85 | pass 86 | -------------------------------------------------------------------------------- /tests/test_util_wim.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing working with WIM files. 8 | """ 9 | 10 | import os.path 11 | from contextlib import contextmanager 12 | 13 | import pytest 14 | 15 | from OPSI.Util.WIM import getImageInformation, parseWIM 16 | 17 | from .helpers import workInTemporaryDirectory, mock 18 | 19 | 20 | @contextmanager 21 | def fakeWIMEnvironment(tempDir=None): 22 | from .conftest import TEST_DATA_PATH 23 | 24 | with workInTemporaryDirectory(tempDir) as temporaryDir: 25 | fakeWimPath = os.path.join(temporaryDir, "fake.wim") 26 | with open(fakeWimPath, "w"): 27 | pass 28 | 29 | exampleData = os.path.join(TEST_DATA_PATH, "wimlib.example") 30 | 31 | def fakeReturningOutput(_unused): 32 | with open(exampleData, "rt", encoding="utf-8") as f: 33 | content = f.read() 34 | return content.split("\n") 35 | 36 | with mock.patch("OPSI.Util.WIM.which", lambda x: "/usr/bin/echo"): 37 | with mock.patch("OPSI.Util.WIM.execute", fakeReturningOutput): 38 | yield fakeWimPath 39 | 40 | 41 | @pytest.fixture 42 | def fakeWimPath(dist_data_path): 43 | with fakeWIMEnvironment(dist_data_path) as fakeWimPath: 44 | yield fakeWimPath 45 | 46 | 47 | def testParsingNonExistingWimFileFails(): 48 | with pytest.raises(OSError): 49 | parseWIM("not_here.wim") 50 | 51 | 52 | def testParsingWIMReturnNoInformationFails(fakeWimPath): 53 | with mock.patch("OPSI.Util.WIM.execute", lambda x: [""]): 54 | with pytest.raises(ValueError): 55 | parseWIM(fakeWimPath) 56 | 57 | 58 | def testParsingWIM(fakeWimPath): 59 | imageData = { 60 | "Windows 7 STARTERN": (set(["de-DE"]), "de-DE"), 61 | "Windows 7 HOMEBASICN": (set(["de-DE"]), "de-DE"), 62 | "Windows 7 HOMEPREMIUMN": (set(["de-DE"]), "de-DE"), 63 | "Windows 7 PROFESSIONALN": (set(["de-DE"]), "de-DE"), 64 | "Windows 7 ULTIMATEN": (set(["de-DE"]), "de-DE"), 65 | } 66 | 67 | for image in parseWIM(fakeWimPath): 68 | assert image.name in imageData 69 | 70 | assert image.languages == imageData[image.name][0] 71 | assert image.default_language == imageData[image.name][1] 72 | 73 | del imageData[image.name] 74 | 75 | assert not imageData, "Missed reading info for {0}".format(imageData.keys()) 76 | 77 | 78 | def testReadingImageInformationFromWim(fakeWimPath): 79 | infos = getImageInformation(fakeWimPath) 80 | 81 | for index in range(5): 82 | print("Check #{}...".format(index)) 83 | info = next(infos) 84 | assert info 85 | 86 | with pytest.raises(StopIteration): # Only five infos in example. 87 | next(infos) 88 | -------------------------------------------------------------------------------- /tests/test_backend_backenddispatcher.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing BackendDispatcher. 8 | """ 9 | 10 | import os 11 | 12 | import pytest 13 | 14 | from OPSI.Backend.BackendManager import BackendDispatcher 15 | from OPSI.Exceptions import BackendConfigurationError 16 | 17 | from .Backends.File import getFileBackend 18 | from .conftest import _backendBase 19 | 20 | 21 | @pytest.mark.parametrize( 22 | "kwargs", 23 | [ 24 | {}, 25 | {"dispatchConfigfile": ""}, 26 | {"dispatchConfigfile": "nope"}, 27 | {"dispatchConfig": ""}, 28 | {"dispatchConfig": [(".*", ("file",))]}, 29 | ], 30 | ) 31 | def testBackendCreationFailsIfConfigMissing(kwargs): 32 | with pytest.raises(BackendConfigurationError): 33 | BackendDispatcher(**kwargs) 34 | 35 | 36 | @pytest.mark.parametrize( 37 | "create_folder", [True, False], ids=["existing folder", "nonexisting folder"] 38 | ) 39 | def testLoadingDispatchConfigFailsIfBackendConfigWithoutConfigs(create_folder, tempDir): 40 | backendDir = os.path.join(tempDir, "backends") 41 | 42 | if create_folder: 43 | os.mkdir(backendDir) 44 | print("Created folder: {0}".format(backendDir)) 45 | 46 | with pytest.raises(BackendConfigurationError): 47 | BackendDispatcher( 48 | dispatchConfig=[[".*", ["file"]]], backendConfigDir=backendDir 49 | ) 50 | 51 | 52 | def testDispatchingMethodAndReceivingResults(dispatcher): 53 | assert [] == dispatcher.host_getObjects() 54 | 55 | 56 | def testLoadingDispatchConfig(dispatcher): 57 | assert "file" in dispatcher.dispatcher_getBackendNames() 58 | assert [(".*", ("file",))] == dispatcher.dispatcher_getConfig() 59 | 60 | 61 | @pytest.fixture 62 | def dispatcherBackend(tempDir): 63 | "A file backend for dispatching" 64 | with getFileBackend(tempDir) as backend: 65 | with _backendBase(backend): 66 | yield backend 67 | 68 | 69 | @pytest.fixture 70 | def dispatcher(dispatcherBackend, tempDir): 71 | "a BackendDispatcher running on a file backend." 72 | 73 | dispatchConfigPath = _patchDispatchConfigForFileBackend(tempDir) 74 | 75 | yield BackendDispatcher( 76 | dispatchConfigFile=dispatchConfigPath, 77 | backendConfigDir=os.path.join(tempDir, "etc", "opsi", "backends"), 78 | ) 79 | 80 | 81 | def _patchDispatchConfigForFileBackend(targetDirectory): 82 | configDir = os.path.join(targetDirectory, "etc", "opsi", "backendManager") 83 | dispatchConfigPath = os.path.join(configDir, "dispatch.conf") 84 | 85 | with open(dispatchConfigPath, "w") as dpconf: 86 | dpconf.write(""" 87 | .* : file 88 | """) 89 | 90 | return dispatchConfigPath 91 | -------------------------------------------------------------------------------- /gettext/python-opsi_en.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2016 uib GmbH 3 | # 4 | # Translators: 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: opsi.org\n" 8 | "POT-Creation-Date: 2015-02-02 10:44+CET\n" 9 | "PO-Revision-Date: 2016-02-29 16:21+0000\n" 10 | "Last-Translator: Niko Wenselowski\n" 11 | "Language-Team: English (http://www.transifex.com/opsi-org/opsiorg/language/en/)\n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: ENCODING\n" 15 | "Generated-By: pygettext.py 1.5\n" 16 | "Language: en\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 18 | 19 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:117 ../OPSI/UI.py:138 ../OPSI/UI.py:141 20 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:147 ../OPSI/UI.py:244 ../OPSI/UI.py:283 21 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:580 ../OPSI/UI.py:680 22 | msgid "OK" 23 | msgstr "OK" 24 | 25 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:244 26 | msgid "An error occured" 27 | msgstr "An error occured" 28 | 29 | #: ../OPSI/UI.py:117 ../OPSI/UI.py:283 30 | msgid "Message" 31 | msgstr "Message" 32 | 33 | #: ../OPSI/UI.py:120 ../OPSI/UI.py:126 ../OPSI/UI.py:322 ../OPSI/UI.py:352 34 | msgid "Progress" 35 | msgstr "Progress" 36 | 37 | #: ../OPSI/UI.py:123 ../OPSI/UI.py:129 ../OPSI/UI.py:337 ../OPSI/UI.py:367 38 | msgid "Copy progress" 39 | msgstr "Copy progress" 40 | 41 | #: ../OPSI/UI.py:132 ../OPSI/UI.py:382 42 | msgid "Text" 43 | msgstr "Text" 44 | 45 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:141 ../OPSI/UI.py:144 ../OPSI/UI.py:147 46 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:580 ../OPSI/UI.py:680 47 | msgid "Cancel" 48 | msgstr "Cancel" 49 | 50 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:396 51 | msgid "Please type text" 52 | msgstr "Please type text" 53 | 54 | #: ../OPSI/UI.py:141 ../OPSI/UI.py:472 55 | msgid "Please select" 56 | msgstr "Please select" 57 | 58 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:580 59 | msgid "Please fill in" 60 | msgstr "Please fill in" 61 | 62 | #: ../OPSI/UI.py:147 ../OPSI/UI.py:680 63 | msgid "Question" 64 | msgstr "Question" 65 | 66 | #: ../OPSI/UI.py:152 ../OPSI/UI.py:169 ../OPSI/UI.py:184 ../OPSI/UI.py:734 67 | #: ../OPSI/UI.py:834 ../OPSI/UI.py:899 68 | msgid "Title" 69 | msgstr "Title" 70 | 71 | #: ../OPSI/UI.py:275 ../OPSI/UI.py:314 72 | msgid " %s | select | scroll text" 73 | msgstr " %s | select | scroll text" 74 | 75 | #: ../OPSI/UI.py:451 ../OPSI/UI.py:553 ../OPSI/UI.py:652 ../OPSI/UI.py:712 76 | msgid " %s | %s | move cursor | select" 77 | msgstr " %s | %s | move cursor | select" 78 | 79 | #: ../OPSI/UI.py:453 ../OPSI/UI.py:555 ../OPSI/UI.py:654 ../OPSI/UI.py:714 80 | msgid " | scroll text" 81 | msgstr " | scroll text" 82 | -------------------------------------------------------------------------------- /tests/test_backend_multithreading.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing backend multithreading behaviour. 8 | """ 9 | 10 | import threading 11 | import time 12 | 13 | import pytest 14 | 15 | from OPSI.Backend.Backend import ExtendedConfigDataBackend 16 | from .test_groups import fillBackendWithObjectToGroups 17 | 18 | 19 | @pytest.mark.parametrize("numberOfThreads", [50]) 20 | def testMultiThreadingBackend(multithreadingBackend, numberOfThreads): 21 | backend = ExtendedConfigDataBackend(multithreadingBackend) 22 | 23 | o2g, _, clients = fillBackendWithObjectToGroups(backend) 24 | 25 | print("====================START=============================") 26 | 27 | class MultiThreadTester(threading.Thread): 28 | def __init__(self, backend, clients, objectToGroups): 29 | threading.Thread.__init__(self) 30 | 31 | self.error = None 32 | 33 | self.backend = backend 34 | self.clients = clients 35 | self.objectToGroups = objectToGroups 36 | 37 | def run(self): 38 | self.client1 = clients[0] 39 | self.client2 = clients[1] 40 | self.objectToGroup1 = o2g[0] 41 | self.objectToGroup2 = o2g[1] 42 | 43 | try: 44 | print("Thread %s started" % self) 45 | time.sleep(1) 46 | self.backend.host_getObjects() 47 | self.backend.host_deleteObjects(self.client1) 48 | 49 | self.backend.host_getObjects() 50 | self.backend.host_deleteObjects(self.client2) 51 | 52 | self.backend.host_createObjects(self.client2) 53 | self.backend.host_createObjects(self.client1) 54 | self.backend.objectToGroup_createObjects(self.objectToGroup1) 55 | self.backend.objectToGroup_createObjects(self.objectToGroup2) 56 | 57 | self.backend.host_getObjects() 58 | self.backend.host_createObjects(self.client1) 59 | self.backend.host_deleteObjects(self.client2) 60 | self.backend.host_createObjects(self.client1) 61 | self.backend.host_getObjects() 62 | except Exception as err: 63 | if "duplicate entry" in str(err).lower(): 64 | # Allow duplicate entry error 65 | pass 66 | else: 67 | self.error = err 68 | finally: 69 | print("Thread %s done" % self) 70 | 71 | mtts = [MultiThreadTester(backend, clients, o2g) for i in range(numberOfThreads)] 72 | for mtt in mtts: 73 | mtt.start() 74 | 75 | for mtt in mtts: 76 | mtt.join() 77 | 78 | client1 = clients[0] 79 | backend.host_createObjects(client1) 80 | 81 | while mtts: 82 | mtt = mtts.pop(0) 83 | if not mtt.is_alive(): 84 | assert not mtt.error, f"Multithreading test failed: Exit Code {mtt.error}" 85 | else: 86 | mtts.append(mtt) 87 | -------------------------------------------------------------------------------- /gettext/python-opsi_da.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # 4 | # Translators: 5 | # Lars Juul , 2016 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: opsi.org\n" 9 | "POT-Creation-Date: 2016-08-24 17:11\n" 10 | "PO-Revision-Date: 2016-11-17 15:41\n" 11 | "Last-Translator: Lars Juul \n" 12 | "Language-Team: Danish (http://www.transifex.com/opsi-org/opsiorg/language/" 13 | "da/)\n" 14 | "Language: da\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: ENCODING\n" 18 | "Generated-By: pygettext.py 1.5\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:117 ../OPSI/UI.py:138 ../OPSI/UI.py:141 22 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:147 ../OPSI/UI.py:244 ../OPSI/UI.py:283 23 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:579 ../OPSI/UI.py:679 24 | msgid "OK" 25 | msgstr "OK" 26 | 27 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:244 28 | msgid "An error occurred" 29 | msgstr "En fejl opstod" 30 | 31 | #: ../OPSI/UI.py:117 ../OPSI/UI.py:283 32 | msgid "Message" 33 | msgstr "Besked" 34 | 35 | #: ../OPSI/UI.py:120 ../OPSI/UI.py:126 ../OPSI/UI.py:322 ../OPSI/UI.py:352 36 | msgid "Progress" 37 | msgstr "Fremskridt" 38 | 39 | #: ../OPSI/UI.py:123 ../OPSI/UI.py:129 ../OPSI/UI.py:337 ../OPSI/UI.py:367 40 | msgid "Copy progress" 41 | msgstr "Kopiere fremskridt" 42 | 43 | #: ../OPSI/UI.py:132 ../OPSI/UI.py:382 44 | msgid "Text" 45 | msgstr "Tekst" 46 | 47 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:141 ../OPSI/UI.py:144 ../OPSI/UI.py:147 48 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:579 ../OPSI/UI.py:679 49 | msgid "Cancel" 50 | msgstr "Afbryd" 51 | 52 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:396 53 | msgid "Please type text" 54 | msgstr "Skriv venligst tekst" 55 | 56 | #: ../OPSI/UI.py:141 ../OPSI/UI.py:472 57 | msgid "Please select" 58 | msgstr "Vælg venligst" 59 | 60 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:579 61 | msgid "Please fill in" 62 | msgstr "Udfyld venligst" 63 | 64 | #: ../OPSI/UI.py:147 ../OPSI/UI.py:679 65 | msgid "Question" 66 | msgstr "Spørgsmål" 67 | 68 | #: ../OPSI/UI.py:152 ../OPSI/UI.py:169 ../OPSI/UI.py:184 ../OPSI/UI.py:733 69 | #: ../OPSI/UI.py:833 ../OPSI/UI.py:898 70 | msgid "Title" 71 | msgstr "Titel" 72 | 73 | #: ../OPSI/UI.py:275 ../OPSI/UI.py:314 74 | msgid " %s | select | scroll text" 75 | msgstr " %s | vælg | scroll tekst" 76 | 77 | #: ../OPSI/UI.py:451 ../OPSI/UI.py:552 ../OPSI/UI.py:651 ../OPSI/UI.py:711 78 | msgid " %s | %s | move cursor | select" 79 | msgstr " %s | %s | flyt cursor | vælg" 80 | 81 | #: ../OPSI/UI.py:453 ../OPSI/UI.py:554 ../OPSI/UI.py:653 ../OPSI/UI.py:713 82 | msgid " | scroll text" 83 | msgstr " | scroll tekst" 84 | -------------------------------------------------------------------------------- /tests/data/backend/testingproduct_23-42.opsi: -------------------------------------------------------------------------------- 1 | 07070200580011000081A400000000000000000000000157E39A9700000800000000FE0000000000000000000000000000000A000254AAOPSI.cpio070702004C02AC000081B0000003E1000003E00000000157E39A97000002E9000000FE0000000000000000000000000000000800010250control[Package] 2 | version: 42 3 | depends: 4 | incremental: False 5 | 6 | [Product] 7 | type: localboot 8 | id: testingproduct 9 | name: python-opsi Test Product 10 | description: Product for testing python-opsi 11 | advice: 12 | version: 23 13 | priority: 0 14 | licenseRequired: False 15 | productClasses: 16 | setupScript: 17 | uninstallScript: 18 | updateScript: 19 | alwaysScript: 20 | onceScript: 21 | customScript: 22 | userLoginScript: 23 | 24 | [ProductDependency] 25 | action: setup 26 | requiredProduct: javavm 27 | requiredStatus: installed 28 | 29 | [ProductProperty] 30 | type: unicode 31 | name: awesome 32 | multivalue: False 33 | editable: True 34 | description: Are you awesome? 35 | values: ["Hell yeah!", "Yes"] 36 | default: ["Yes"] 37 | 38 | [Changelog] 39 | testingproduct (23-42) testing; urgency=low 40 | 41 | * Initial package 42 | 43 | -- Niko Wenselowski Wed, 21 Sep 2016 11:59:44 +0000 44 | 45 | 46 | 070702004C02AD000081B0000003E1000003E00000000157E25A100000014C000000FE0000000000000000000000000000000800007267preinst#! /bin/bash 47 | # 48 | # preinst script 49 | # This script executes before that package will be unpacked from its archive file. 50 | # 51 | # The following environment variables can be used to obtain information about the current installation: 52 | # PRODUCT_ID: id of the current product 53 | # CLIENT_DATA_DIR: directory where client data will be installed 54 | # 55 | 070702004C02AE000081B0000003E1000003E00000000157E25A1000000168000000FE0000000000000000000000000000000900007D95postinst#! /bin/bash 56 | # 57 | # postinst script 58 | # This script executes after unpacking files from that archive and registering the product at the depot. 59 | # 60 | # The following environment variables can be used to obtain information about the current installation: 61 | # PRODUCT_ID: id of the current product 62 | # CLIENT_DATA_DIR: directory which contains the installed client data 63 | # 64 | 07070200000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!07070200000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!! -------------------------------------------------------------------------------- /gettext/python-opsi_it.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # 4 | # Translators: 5 | # Tommaso Amici , 2015 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: opsi.org\n" 9 | "POT-Creation-Date: 2015-02-02 10:44+CET\n" 10 | "PO-Revision-Date: 2015-02-25 15:11+0000\n" 11 | "Last-Translator: Niko Wenselowski\n" 12 | "Language-Team: Italian (http://www.transifex.com/opsi-org/opsiorg/language/it/)\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: ENCODING\n" 16 | "Generated-By: pygettext.py 1.5\n" 17 | "Language: it\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:117 ../OPSI/UI.py:138 ../OPSI/UI.py:141 21 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:147 ../OPSI/UI.py:244 ../OPSI/UI.py:283 22 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:580 ../OPSI/UI.py:680 23 | msgid "OK" 24 | msgstr "OK" 25 | 26 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:244 27 | msgid "An error occured" 28 | msgstr "È avvenuto un errore" 29 | 30 | #: ../OPSI/UI.py:117 ../OPSI/UI.py:283 31 | msgid "Message" 32 | msgstr "Messaggio" 33 | 34 | #: ../OPSI/UI.py:120 ../OPSI/UI.py:126 ../OPSI/UI.py:322 ../OPSI/UI.py:352 35 | msgid "Progress" 36 | msgstr "Completamento" 37 | 38 | #: ../OPSI/UI.py:123 ../OPSI/UI.py:129 ../OPSI/UI.py:337 ../OPSI/UI.py:367 39 | msgid "Copy progress" 40 | msgstr "Completamento della copia" 41 | 42 | #: ../OPSI/UI.py:132 ../OPSI/UI.py:382 43 | msgid "Text" 44 | msgstr "Testo" 45 | 46 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:141 ../OPSI/UI.py:144 ../OPSI/UI.py:147 47 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:580 ../OPSI/UI.py:680 48 | msgid "Cancel" 49 | msgstr "Annulla" 50 | 51 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:396 52 | msgid "Please type text" 53 | msgstr "Per favore, inserire testo" 54 | 55 | #: ../OPSI/UI.py:141 ../OPSI/UI.py:472 56 | msgid "Please select" 57 | msgstr "Per favore, seleziona" 58 | 59 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:580 60 | msgid "Please fill in" 61 | msgstr "Per favore, compila" 62 | 63 | #: ../OPSI/UI.py:147 ../OPSI/UI.py:680 64 | msgid "Question" 65 | msgstr "Domanda" 66 | 67 | #: ../OPSI/UI.py:152 ../OPSI/UI.py:169 ../OPSI/UI.py:184 ../OPSI/UI.py:734 68 | #: ../OPSI/UI.py:834 ../OPSI/UI.py:899 69 | msgid "Title" 70 | msgstr "Titolo" 71 | 72 | #: ../OPSI/UI.py:275 ../OPSI/UI.py:314 73 | msgid " %s | select | scroll text" 74 | msgstr " %s | seleziona | scorri testo" 75 | 76 | #: ../OPSI/UI.py:451 ../OPSI/UI.py:553 ../OPSI/UI.py:652 ../OPSI/UI.py:712 77 | msgid " %s | %s | move cursor | select" 78 | msgstr " %s | %s | muovi il cursore | seleziona" 79 | 80 | #: ../OPSI/UI.py:453 ../OPSI/UI.py:555 ../OPSI/UI.py:654 ../OPSI/UI.py:714 81 | msgid " | scroll text" 82 | msgstr "| scorri testo" 83 | -------------------------------------------------------------------------------- /tests/test_util_task_update_backend_file.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing the update of the MySQL backend from an older version. 8 | """ 9 | 10 | import json 11 | import os.path 12 | 13 | import pytest 14 | 15 | from OPSI.Util.Task.UpdateBackend.File import ( 16 | FileBackendUpdateError, 17 | getVersionFilePath, 18 | readBackendVersion, 19 | _readVersionFile, 20 | updateBackendVersion, 21 | updateFileBackend, 22 | ) 23 | 24 | from .Backends.File import getFileBackend 25 | 26 | 27 | @pytest.fixture 28 | def fileBackend(tempDir): 29 | with getFileBackend(path=tempDir) as backend: 30 | yield backend 31 | 32 | 33 | def testUpdatingFileBackend(fileBackend, tempDir): 34 | config = os.path.join(tempDir, "etc", "opsi", "backends", "file.conf") 35 | 36 | updateFileBackend(config) 37 | 38 | 39 | def testReadingSchemaVersionFromMissingFile(tempDir): 40 | assert readBackendVersion(os.path.join(tempDir, "missingbasedir")) is None 41 | 42 | 43 | @pytest.fixture 44 | def baseDirectory(tempDir): 45 | newDir = os.path.join(tempDir, "config") 46 | os.makedirs(newDir) 47 | yield newDir 48 | 49 | 50 | @pytest.fixture 51 | def writtenConfig(baseDirectory): 52 | configFile = getVersionFilePath(baseDirectory) 53 | config = { 54 | 0: { 55 | "start": 1495529319.022833, 56 | "end": 1495529341.870662, 57 | }, 58 | 1: {"start": 1495539432.271123, "end": 1495539478.045244}, 59 | } 60 | with open(configFile, "w") as f: 61 | json.dump(config, f) 62 | 63 | yield config 64 | 65 | 66 | def testReadingSchemaVersionLowLevel(baseDirectory, writtenConfig): 67 | assert writtenConfig == _readVersionFile(baseDirectory) 68 | 69 | 70 | def testReadingSchemaVersion(baseDirectory, writtenConfig): 71 | version = readBackendVersion(baseDirectory) 72 | assert version is not None 73 | assert version == max(writtenConfig.keys()) 74 | assert version > 0 75 | 76 | 77 | @pytest.mark.parametrize( 78 | "config", 79 | [ 80 | {0: {"start": 1495529319.022833}}, # missing end 81 | {0: {}}, # missing start 82 | ], 83 | ) 84 | def testRaisingExceptionOnUnfinishedUpdate(baseDirectory, config): 85 | configFile = getVersionFilePath(baseDirectory) 86 | 87 | with open(configFile, "w") as f: 88 | json.dump(config, f) 89 | 90 | with pytest.raises(FileBackendUpdateError): 91 | readBackendVersion(baseDirectory) 92 | 93 | 94 | def testApplyingTheSameUpdateMultipleTimesFails(baseDirectory): 95 | with updateBackendVersion(baseDirectory, 1): 96 | pass 97 | 98 | with pytest.raises(FileBackendUpdateError): 99 | with updateBackendVersion(baseDirectory, 1): 100 | pass 101 | -------------------------------------------------------------------------------- /gettext/python-opsi_fr.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # 4 | # Translators: 5 | # Automatically generated, 2011 6 | # Daniel Debeus, 2016 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: opsi.org\n" 10 | "POT-Creation-Date: 2016-08-24 17:11\n" 11 | "PO-Revision-Date: 2016-08-26 08:12\n" 12 | "Last-Translator: Daniel Debeus\n" 13 | "Language-Team: French (http://www.transifex.com/opsi-org/opsiorg/language/" 14 | "fr/)\n" 15 | "Language: fr\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: ENCODING\n" 19 | "Generated-By: pygettext.py 1.5\n" 20 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 21 | 22 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:117 ../OPSI/UI.py:138 ../OPSI/UI.py:141 23 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:147 ../OPSI/UI.py:244 ../OPSI/UI.py:283 24 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:579 ../OPSI/UI.py:679 25 | msgid "OK" 26 | msgstr "OK" 27 | 28 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:244 29 | msgid "An error occurred" 30 | msgstr "Une erreur est survenue" 31 | 32 | #: ../OPSI/UI.py:117 ../OPSI/UI.py:283 33 | msgid "Message" 34 | msgstr "Message" 35 | 36 | #: ../OPSI/UI.py:120 ../OPSI/UI.py:126 ../OPSI/UI.py:322 ../OPSI/UI.py:352 37 | msgid "Progress" 38 | msgstr "Progression" 39 | 40 | #: ../OPSI/UI.py:123 ../OPSI/UI.py:129 ../OPSI/UI.py:337 ../OPSI/UI.py:367 41 | msgid "Copy progress" 42 | msgstr "Progression de la copie" 43 | 44 | #: ../OPSI/UI.py:132 ../OPSI/UI.py:382 45 | msgid "Text" 46 | msgstr "Texte" 47 | 48 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:141 ../OPSI/UI.py:144 ../OPSI/UI.py:147 49 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:579 ../OPSI/UI.py:679 50 | msgid "Cancel" 51 | msgstr "Annuler" 52 | 53 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:396 54 | msgid "Please type text" 55 | msgstr "Veuillez entrer le texte" 56 | 57 | #: ../OPSI/UI.py:141 ../OPSI/UI.py:472 58 | msgid "Please select" 59 | msgstr "Veuillez sélectionner" 60 | 61 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:579 62 | msgid "Please fill in" 63 | msgstr "Veuillez remplir" 64 | 65 | #: ../OPSI/UI.py:147 ../OPSI/UI.py:679 66 | msgid "Question" 67 | msgstr "Question" 68 | 69 | #: ../OPSI/UI.py:152 ../OPSI/UI.py:169 ../OPSI/UI.py:184 ../OPSI/UI.py:733 70 | #: ../OPSI/UI.py:833 ../OPSI/UI.py:898 71 | msgid "Title" 72 | msgstr "Titre" 73 | 74 | #: ../OPSI/UI.py:275 ../OPSI/UI.py:314 75 | msgid " %s | select | scroll text" 76 | msgstr " %s | sélectionner | défilement du texte" 77 | 78 | #: ../OPSI/UI.py:451 ../OPSI/UI.py:552 ../OPSI/UI.py:651 ../OPSI/UI.py:711 79 | msgid " %s | %s | move cursor | select" 80 | msgstr " %s | %s | bouger le curseur | sélectionner" 81 | 82 | #: ../OPSI/UI.py:453 ../OPSI/UI.py:554 ../OPSI/UI.py:653 ../OPSI/UI.py:713 83 | msgid " | scroll text" 84 | msgstr " | défilement du texte" 85 | -------------------------------------------------------------------------------- /gettext/python-opsi_de.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # 4 | # Translators: 5 | # Automatically generated, 2010 6 | # Ettore Atalan , 2016 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: opsi.org\n" 10 | "POT-Creation-Date: 2016-08-24 17:11\n" 11 | "PO-Revision-Date: 2016-08-29 23:27\n" 12 | "Last-Translator: Ettore Atalan \n" 13 | "Language-Team: German (http://www.transifex.com/opsi-org/opsiorg/language/" 14 | "de/)\n" 15 | "Language: de\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: ENCODING\n" 19 | "Generated-By: pygettext.py 1.5\n" 20 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 21 | 22 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:117 ../OPSI/UI.py:138 ../OPSI/UI.py:141 23 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:147 ../OPSI/UI.py:244 ../OPSI/UI.py:283 24 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:579 ../OPSI/UI.py:679 25 | msgid "OK" 26 | msgstr "OK" 27 | 28 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:244 29 | msgid "An error occurred" 30 | msgstr "Ein Fehler ist aufgetreten" 31 | 32 | #: ../OPSI/UI.py:117 ../OPSI/UI.py:283 33 | msgid "Message" 34 | msgstr "Nachricht" 35 | 36 | #: ../OPSI/UI.py:120 ../OPSI/UI.py:126 ../OPSI/UI.py:322 ../OPSI/UI.py:352 37 | msgid "Progress" 38 | msgstr "Fortschritt" 39 | 40 | #: ../OPSI/UI.py:123 ../OPSI/UI.py:129 ../OPSI/UI.py:337 ../OPSI/UI.py:367 41 | msgid "Copy progress" 42 | msgstr "Kopier-Fortschritt" 43 | 44 | #: ../OPSI/UI.py:132 ../OPSI/UI.py:382 45 | msgid "Text" 46 | msgstr "Text" 47 | 48 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:141 ../OPSI/UI.py:144 ../OPSI/UI.py:147 49 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:579 ../OPSI/UI.py:679 50 | msgid "Cancel" 51 | msgstr "Abbrechen" 52 | 53 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:396 54 | msgid "Please type text" 55 | msgstr "Bitte Text eingeben" 56 | 57 | #: ../OPSI/UI.py:141 ../OPSI/UI.py:472 58 | msgid "Please select" 59 | msgstr "Bitte auswählen" 60 | 61 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:579 62 | msgid "Please fill in" 63 | msgstr "Bitte ausfüllen" 64 | 65 | #: ../OPSI/UI.py:147 ../OPSI/UI.py:679 66 | msgid "Question" 67 | msgstr "Frage" 68 | 69 | #: ../OPSI/UI.py:152 ../OPSI/UI.py:169 ../OPSI/UI.py:184 ../OPSI/UI.py:733 70 | #: ../OPSI/UI.py:833 ../OPSI/UI.py:898 71 | msgid "Title" 72 | msgstr "Titel" 73 | 74 | #: ../OPSI/UI.py:275 ../OPSI/UI.py:314 75 | msgid " %s | select | scroll text" 76 | msgstr " %s | wählen | Text scrollen" 77 | 78 | #: ../OPSI/UI.py:451 ../OPSI/UI.py:552 ../OPSI/UI.py:651 ../OPSI/UI.py:711 79 | msgid " %s | %s | move cursor | select" 80 | msgstr " %s | %s | bewegen | wählen" 81 | 82 | #: ../OPSI/UI.py:453 ../OPSI/UI.py:554 ../OPSI/UI.py:653 ../OPSI/UI.py:713 83 | msgid " | scroll text" 84 | msgstr " | Text scrollen" 85 | -------------------------------------------------------------------------------- /gettext/python-opsi_es.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # 4 | # Translators: 5 | # Martin Scalese , 2015,2017 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: opsi.org\n" 9 | "POT-Creation-Date: 2016-08-24 17:11+CEST\n" 10 | "PO-Revision-Date: 2017-08-07 11:20+0000\n" 11 | "Last-Translator: Martin Scalese \n" 12 | "Language-Team: Spanish (http://www.transifex.com/opsi-org/opsiorg/language/es/)\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: ENCODING\n" 16 | "Generated-By: pygettext.py 1.5\n" 17 | "Language: es\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:117 ../OPSI/UI.py:138 ../OPSI/UI.py:141 21 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:147 ../OPSI/UI.py:244 ../OPSI/UI.py:283 22 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:579 ../OPSI/UI.py:679 23 | msgid "OK" 24 | msgstr "OK" 25 | 26 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:244 27 | msgid "An error occurred" 28 | msgstr "Un error ocurrió" 29 | 30 | #: ../OPSI/UI.py:117 ../OPSI/UI.py:283 31 | msgid "Message" 32 | msgstr "Mensaje" 33 | 34 | #: ../OPSI/UI.py:120 ../OPSI/UI.py:126 ../OPSI/UI.py:322 ../OPSI/UI.py:352 35 | msgid "Progress" 36 | msgstr "Progreso" 37 | 38 | #: ../OPSI/UI.py:123 ../OPSI/UI.py:129 ../OPSI/UI.py:337 ../OPSI/UI.py:367 39 | msgid "Copy progress" 40 | msgstr "Progreso de copia" 41 | 42 | #: ../OPSI/UI.py:132 ../OPSI/UI.py:382 43 | msgid "Text" 44 | msgstr "Texto" 45 | 46 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:141 ../OPSI/UI.py:144 ../OPSI/UI.py:147 47 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:579 ../OPSI/UI.py:679 48 | msgid "Cancel" 49 | msgstr "Cancelar" 50 | 51 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:396 52 | msgid "Please type text" 53 | msgstr "Por favor introduzca el texto" 54 | 55 | #: ../OPSI/UI.py:141 ../OPSI/UI.py:472 56 | msgid "Please select" 57 | msgstr "Por favor seleccione" 58 | 59 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:579 60 | msgid "Please fill in" 61 | msgstr "Por favor rellene el campo" 62 | 63 | #: ../OPSI/UI.py:147 ../OPSI/UI.py:679 64 | msgid "Question" 65 | msgstr "Pregunta" 66 | 67 | #: ../OPSI/UI.py:152 ../OPSI/UI.py:169 ../OPSI/UI.py:184 ../OPSI/UI.py:733 68 | #: ../OPSI/UI.py:833 ../OPSI/UI.py:898 69 | msgid "Title" 70 | msgstr "Titulo" 71 | 72 | #: ../OPSI/UI.py:275 ../OPSI/UI.py:314 73 | msgid " %s | select | scroll text" 74 | msgstr " %s | seleccionar | desplazarse en el texto" 75 | 76 | #: ../OPSI/UI.py:451 ../OPSI/UI.py:552 ../OPSI/UI.py:651 ../OPSI/UI.py:711 77 | msgid " %s | %s | move cursor | select" 78 | msgstr " %s | %s | mover el cursor | seleccionar" 79 | 80 | #: ../OPSI/UI.py:453 ../OPSI/UI.py:554 ../OPSI/UI.py:653 ../OPSI/UI.py:713 81 | msgid " | scroll text" 82 | msgstr " | desplazarse en el texto" 83 | -------------------------------------------------------------------------------- /gettext/python-opsi_pl.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2016 uib GmbH 3 | # 4 | # Translators: 5 | # Jerzy Włudarczylk , 2016 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: opsi.org\n" 9 | "POT-Creation-Date: 2015-02-02 10:44+CET\n" 10 | "PO-Revision-Date: 2016-02-29 16:21+0000\n" 11 | "Last-Translator: Jerzy Włudarczylk \n" 12 | "Language-Team: Polish (http://www.transifex.com/opsi-org/opsiorg/language/pl/)\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: ENCODING\n" 16 | "Generated-By: pygettext.py 1.5\n" 17 | "Language: pl\n" 18 | "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 19 | 20 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:117 ../OPSI/UI.py:138 ../OPSI/UI.py:141 21 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:147 ../OPSI/UI.py:244 ../OPSI/UI.py:283 22 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:580 ../OPSI/UI.py:680 23 | msgid "OK" 24 | msgstr "OK" 25 | 26 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:244 27 | msgid "An error occured" 28 | msgstr "Wystąpił błąd" 29 | 30 | #: ../OPSI/UI.py:117 ../OPSI/UI.py:283 31 | msgid "Message" 32 | msgstr "Wiadomość" 33 | 34 | #: ../OPSI/UI.py:120 ../OPSI/UI.py:126 ../OPSI/UI.py:322 ../OPSI/UI.py:352 35 | msgid "Progress" 36 | msgstr "Postęp" 37 | 38 | #: ../OPSI/UI.py:123 ../OPSI/UI.py:129 ../OPSI/UI.py:337 ../OPSI/UI.py:367 39 | msgid "Copy progress" 40 | msgstr "Postęp kopiowania" 41 | 42 | #: ../OPSI/UI.py:132 ../OPSI/UI.py:382 43 | msgid "Text" 44 | msgstr "Tekst" 45 | 46 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:141 ../OPSI/UI.py:144 ../OPSI/UI.py:147 47 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:580 ../OPSI/UI.py:680 48 | msgid "Cancel" 49 | msgstr "Anuluj" 50 | 51 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:396 52 | msgid "Please type text" 53 | msgstr "Wpisz tekst" 54 | 55 | #: ../OPSI/UI.py:141 ../OPSI/UI.py:472 56 | msgid "Please select" 57 | msgstr "Wybierz" 58 | 59 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:580 60 | msgid "Please fill in" 61 | msgstr "Proszę wypełnić" 62 | 63 | #: ../OPSI/UI.py:147 ../OPSI/UI.py:680 64 | msgid "Question" 65 | msgstr "Pytanie" 66 | 67 | #: ../OPSI/UI.py:152 ../OPSI/UI.py:169 ../OPSI/UI.py:184 ../OPSI/UI.py:734 68 | #: ../OPSI/UI.py:834 ../OPSI/UI.py:899 69 | msgid "Title" 70 | msgstr "Tytuł" 71 | 72 | #: ../OPSI/UI.py:275 ../OPSI/UI.py:314 73 | msgid " %s | select | scroll text" 74 | msgstr " %s | wybierz | przewiń tekst" 75 | 76 | #: ../OPSI/UI.py:451 ../OPSI/UI.py:553 ../OPSI/UI.py:652 ../OPSI/UI.py:712 77 | msgid " %s | %s | move cursor | select" 78 | msgstr " %s | %s | przesuń kursor | wybierz" 79 | 80 | #: ../OPSI/UI.py:453 ../OPSI/UI.py:555 ../OPSI/UI.py:654 ../OPSI/UI.py:714 81 | msgid " | scroll text" 82 | msgstr "| przewiń tekst" 83 | -------------------------------------------------------------------------------- /gettext/python-opsi_nl.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # 4 | # Translators: 5 | # Peter De Ridder , 2017 6 | # Selina Oudermans , 2017 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: opsi.org\n" 10 | "POT-Creation-Date: 2016-08-24 17:11+CEST\n" 11 | "PO-Revision-Date: 2017-08-07 11:20+0000\n" 12 | "Last-Translator: Peter De Ridder \n" 13 | "Language-Team: Dutch (http://www.transifex.com/opsi-org/opsiorg/language/nl/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: ENCODING\n" 17 | "Generated-By: pygettext.py 1.5\n" 18 | "Language: nl\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:117 ../OPSI/UI.py:138 ../OPSI/UI.py:141 22 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:147 ../OPSI/UI.py:244 ../OPSI/UI.py:283 23 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:579 ../OPSI/UI.py:679 24 | msgid "OK" 25 | msgstr "OK" 26 | 27 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:244 28 | msgid "An error occurred" 29 | msgstr "Er is een fout opgetreden" 30 | 31 | #: ../OPSI/UI.py:117 ../OPSI/UI.py:283 32 | msgid "Message" 33 | msgstr "Bericht" 34 | 35 | #: ../OPSI/UI.py:120 ../OPSI/UI.py:126 ../OPSI/UI.py:322 ../OPSI/UI.py:352 36 | msgid "Progress" 37 | msgstr "Voortgang" 38 | 39 | #: ../OPSI/UI.py:123 ../OPSI/UI.py:129 ../OPSI/UI.py:337 ../OPSI/UI.py:367 40 | msgid "Copy progress" 41 | msgstr "Voortgang van kopiëren" 42 | 43 | #: ../OPSI/UI.py:132 ../OPSI/UI.py:382 44 | msgid "Text" 45 | msgstr "Tekst" 46 | 47 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:141 ../OPSI/UI.py:144 ../OPSI/UI.py:147 48 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:579 ../OPSI/UI.py:679 49 | msgid "Cancel" 50 | msgstr "Annuleren" 51 | 52 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:396 53 | msgid "Please type text" 54 | msgstr "Type de tekst" 55 | 56 | #: ../OPSI/UI.py:141 ../OPSI/UI.py:472 57 | msgid "Please select" 58 | msgstr "Selecteer" 59 | 60 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:579 61 | msgid "Please fill in" 62 | msgstr "Vul dit in" 63 | 64 | #: ../OPSI/UI.py:147 ../OPSI/UI.py:679 65 | msgid "Question" 66 | msgstr "Vraag" 67 | 68 | #: ../OPSI/UI.py:152 ../OPSI/UI.py:169 ../OPSI/UI.py:184 ../OPSI/UI.py:733 69 | #: ../OPSI/UI.py:833 ../OPSI/UI.py:898 70 | msgid "Title" 71 | msgstr "Titel" 72 | 73 | #: ../OPSI/UI.py:275 ../OPSI/UI.py:314 74 | msgid " %s | select | scroll text" 75 | msgstr " %s | selecteer | scroll text" 76 | 77 | #: ../OPSI/UI.py:451 ../OPSI/UI.py:552 ../OPSI/UI.py:651 ../OPSI/UI.py:711 78 | msgid " %s | %s | move cursor | select" 79 | msgstr " %s | %s | verplaats cursor | selecteer" 80 | 81 | #: ../OPSI/UI.py:453 ../OPSI/UI.py:554 ../OPSI/UI.py:653 ../OPSI/UI.py:713 82 | msgid " | scroll text" 83 | msgstr "| scroll text" 84 | -------------------------------------------------------------------------------- /tests/test_backend_modificationtracker.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing the modification tracking. 8 | 9 | Based on work of Christian Kampka. 10 | """ 11 | 12 | from OPSI.Backend.Backend import ModificationTrackingBackend 13 | from OPSI.Object import OpsiClient 14 | 15 | from .Backends.SQLite import getSQLiteBackend, getSQLiteModificationTracker 16 | from .Backends.MySQL import getMySQLBackend, getMySQLModificationTracker 17 | 18 | import pytest 19 | 20 | 21 | @pytest.fixture( 22 | params=[ 23 | (getSQLiteBackend, getSQLiteModificationTracker), 24 | (getMySQLBackend, getMySQLModificationTracker), 25 | ], 26 | ids=["sqlite", "mysql"], 27 | ) 28 | def backendAndTracker(request): 29 | backendFunc, trackerFunc = request.param 30 | with backendFunc() as basebackend: 31 | basebackend.backend_createBase() 32 | 33 | backend = ModificationTrackingBackend(basebackend) 34 | 35 | with trackerFunc() as tracker: 36 | backend.addBackendChangeListener(tracker) 37 | 38 | yield backend, tracker 39 | 40 | # When reusing a database there may be leftover modifications! 41 | tracker.clearModifications() 42 | 43 | backend.backend_deleteBase() 44 | 45 | 46 | def testTrackingOfInsertObject(backendAndTracker): 47 | backend, tracker = backendAndTracker 48 | 49 | host = OpsiClient(id="client1.test.invalid") 50 | backend.host_insertObject(host) 51 | 52 | modifications = tracker.getModifications() 53 | assert 1 == len(modifications) 54 | mod = modifications[0] 55 | assert mod["objectClass"] == host.__class__.__name__ 56 | assert mod["command"] == "insert" 57 | assert mod["ident"] == host.getIdent() 58 | 59 | 60 | def testTrackingOfUpdatingObject(backendAndTracker): 61 | backend, tracker = backendAndTracker 62 | 63 | host = OpsiClient(id="client1.test.invalid") 64 | 65 | backend.host_insertObject(host) 66 | tracker.clearModifications() 67 | backend.host_updateObject(host) 68 | 69 | modifications = tracker.getModifications() 70 | assert 1 == len(modifications) 71 | mod = modifications[0] 72 | assert mod["objectClass"] == host.__class__.__name__ 73 | assert mod["command"] == "update" 74 | assert mod["ident"] == host.getIdent() 75 | 76 | 77 | @pytest.mark.requires_license_file 78 | def testTrackingOfDeletingObject(backendAndTracker): 79 | backend, tracker = backendAndTracker 80 | 81 | host = OpsiClient(id="client1.test.invalid") 82 | 83 | backend.host_insertObject(host) 84 | tracker.clearModifications() 85 | backend.host_deleteObjects(host) 86 | 87 | modifications = tracker.getModifications() 88 | 89 | assert 1 == len(modifications) 90 | modification = modifications[0] 91 | 92 | assert modification["objectClass"] == host.__class__.__name__ 93 | assert modification["command"] == "delete" 94 | assert modification["ident"] == host.getIdent() 95 | -------------------------------------------------------------------------------- /data/backendManager/extend.d/10_wim.conf: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def updateWIMConfig(self, productId): 4 | """ 5 | Update the configuration of a Windows netboot product based on the \ 6 | information in it's install.wim. 7 | 8 | IMPORTANT: This does only work on the configserver! 9 | """ 10 | import os.path 11 | from OPSI.Types import forceProductId 12 | from OPSI.Util import getfqdn 13 | 14 | if not productId: 15 | raise ValueError("Missing productId: {0!r}".format(productId)) 16 | 17 | productId = forceProductId(productId) 18 | 19 | if not self.product_getObjects(id=productId): 20 | from OPSI.Exceptions import BackendMissingDataError 21 | 22 | raise BackendMissingDataError("No product with ID {0!r}".format(productId)) 23 | 24 | depotId = getfqdn() 25 | if not self.productOnDepot_getObjects(depotId=depotId, productId=productId): 26 | from OPSI.Exceptions import BackendMissingDataError 27 | 28 | raise BackendMissingDataError("No product {0!r} on {1!r}".format(productId, depotId)) 29 | 30 | depot = self.host_getObjects(id=depotId, type="OpsiDepotserver") 31 | depot = depot[0] 32 | logger.debug("Working with %s", depot) 33 | 34 | depotPath = depot.depotLocalUrl 35 | if not depotPath.startswith('file://'): 36 | raise ValueError("Unable to handle the depot remote local url {0!r}.".format(depotPath)) 37 | 38 | depotPath = depotPath[7:] 39 | logger.debug("Created path %s", depotPath) 40 | productPath = os.path.join(depotPath, productId) 41 | wimSearchPath = os.path.join(productPath, 'installfiles', 'sources') 42 | 43 | for filename in ('install.wim', 'install.esd'): 44 | wimPath = os.path.join(wimSearchPath, filename) 45 | 46 | if os.path.exists(wimPath): 47 | logger.debug("Found image file %s", filename) 48 | break 49 | else: 50 | raise IOError("Unable to find install.wim / install.esd in {0!r}".format(wimSearchPath)) 51 | 52 | self.updateWIMConfigFromPath(wimPath, productId) 53 | 54 | def updateWIMConfigFromPath(self, path, targetProductId): 55 | """ 56 | Update the configuration of `targetProductId` based on the \ 57 | information in the install.wim at the given `path`. 58 | 59 | IMPORTANT: This does only work on the configserver! 60 | """ 61 | import itertools 62 | from OPSI.Util.WIM import parseWIM, writeImageInformation 63 | 64 | images = parseWIM(path) 65 | 66 | if targetProductId: 67 | defaultLanguage = set([image.default_language for image in images if image.default_language]) 68 | if 1 == len(defaultLanguage): 69 | defaultLanguage = defaultLanguage.pop() 70 | else: 71 | if len(defaultLanguage) > 1: 72 | logger.info("Multiple default languages: %s", defaultLanguage) 73 | logger.info("Not setting a default.") 74 | else: 75 | logger.info("Unable to find a default language.") 76 | 77 | defaultLanguage = None 78 | 79 | writeImageInformation(self, targetProductId, [image.name for image in images], 80 | list(set(itertools.chain(*[image.languages for image in images if image.languages]))), 81 | defaultLanguage) 82 | -------------------------------------------------------------------------------- /gettext/python-opsi_ru.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # 4 | # Translators: 5 | # Alexander Savchenko, 2014 6 | # Вячеслав Сухарников, 2016-2017 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: opsi.org\n" 10 | "POT-Creation-Date: 2016-08-24 17:11+CEST\n" 11 | "PO-Revision-Date: 2017-08-07 11:20+0000\n" 12 | "Last-Translator: Вячеслав Сухарников\n" 13 | "Language-Team: Russian (http://www.transifex.com/opsi-org/opsiorg/language/ru/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: ENCODING\n" 17 | "Generated-By: pygettext.py 1.5\n" 18 | "Language: ru\n" 19 | "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" 20 | 21 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:117 ../OPSI/UI.py:138 ../OPSI/UI.py:141 22 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:147 ../OPSI/UI.py:244 ../OPSI/UI.py:283 23 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:579 ../OPSI/UI.py:679 24 | msgid "OK" 25 | msgstr "ОК" 26 | 27 | #: ../OPSI/UI.py:114 ../OPSI/UI.py:244 28 | msgid "An error occurred" 29 | msgstr "Произошла ошибка" 30 | 31 | #: ../OPSI/UI.py:117 ../OPSI/UI.py:283 32 | msgid "Message" 33 | msgstr "Сообщение" 34 | 35 | #: ../OPSI/UI.py:120 ../OPSI/UI.py:126 ../OPSI/UI.py:322 ../OPSI/UI.py:352 36 | msgid "Progress" 37 | msgstr "Выполняется" 38 | 39 | #: ../OPSI/UI.py:123 ../OPSI/UI.py:129 ../OPSI/UI.py:337 ../OPSI/UI.py:367 40 | msgid "Copy progress" 41 | msgstr "Копируется" 42 | 43 | #: ../OPSI/UI.py:132 ../OPSI/UI.py:382 44 | msgid "Text" 45 | msgstr "Текст" 46 | 47 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:141 ../OPSI/UI.py:144 ../OPSI/UI.py:147 48 | #: ../OPSI/UI.py:396 ../OPSI/UI.py:472 ../OPSI/UI.py:579 ../OPSI/UI.py:679 49 | msgid "Cancel" 50 | msgstr "Отменить" 51 | 52 | #: ../OPSI/UI.py:138 ../OPSI/UI.py:396 53 | msgid "Please type text" 54 | msgstr "Пожалуйста, введите текст" 55 | 56 | #: ../OPSI/UI.py:141 ../OPSI/UI.py:472 57 | msgid "Please select" 58 | msgstr "Пожалуйста, выберите" 59 | 60 | #: ../OPSI/UI.py:144 ../OPSI/UI.py:579 61 | msgid "Please fill in" 62 | msgstr "Пожалуйста, заполните" 63 | 64 | #: ../OPSI/UI.py:147 ../OPSI/UI.py:679 65 | msgid "Question" 66 | msgstr "Вопрос" 67 | 68 | #: ../OPSI/UI.py:152 ../OPSI/UI.py:169 ../OPSI/UI.py:184 ../OPSI/UI.py:733 69 | #: ../OPSI/UI.py:833 ../OPSI/UI.py:898 70 | msgid "Title" 71 | msgstr "Заголовок" 72 | 73 | #: ../OPSI/UI.py:275 ../OPSI/UI.py:314 74 | msgid " %s | select | scroll text" 75 | msgstr " %s | выбрать | листать текст" 76 | 77 | #: ../OPSI/UI.py:451 ../OPSI/UI.py:552 ../OPSI/UI.py:651 ../OPSI/UI.py:711 78 | msgid " %s | %s | move cursor | select" 79 | msgstr " %s | %s | переместить курсор | выбрать" 80 | 81 | #: ../OPSI/UI.py:453 ../OPSI/UI.py:554 ../OPSI/UI.py:653 ../OPSI/UI.py:713 82 | msgid " | scroll text" 83 | msgstr " | листать текст" 84 | -------------------------------------------------------------------------------- /tests/data/util/file/inf_testdata_2.inf: -------------------------------------------------------------------------------- 1 | 2 | ; SMBUSati.inf 3 | ; 4 | ; Installation file (.inf) for the ATI SMBus device. 5 | ; 6 | ; (c) Copyright 2002-2006 ATI Technologies Inc 7 | ; 8 | 9 | [Version] 10 | Signature="$CHICAGO$" 11 | Provider=%ATI% 12 | ClassGUID={4d36e97d-e325-11ce-bfc1-08002be10318} 13 | Class=System 14 | CatalogFile=SMbusati.cat 15 | DriverVer=02/26/2007,5.10.1000.8 16 | 17 | [DestinationDirs] 18 | DefaultDestDir = 12 19 | 20 | ; 21 | ; Driver information 22 | ; 23 | 24 | [Manufacturer] 25 | %ATI% = ATI.Mfg, NTamd64 26 | 27 | 28 | [ATI.Mfg] 29 | %ATI.DeviceDesc0% = ATISMBus, PCI\VEN_1002&DEV_4353 30 | %ATI.DeviceDesc0% = ATISMBus, PCI\VEN_1002&DEV_4363 31 | %ATI.DeviceDesc0% = ATISMBus, PCI\VEN_1002&DEV_4372 32 | %ATI.DeviceDesc0% = ATISMBus, PCI\VEN_1002&DEV_4385 33 | 34 | [ATI.Mfg.NTamd64] 35 | %ATI.DeviceDesc0% = ATISMBus64, PCI\VEN_1002&DEV_4353 36 | %ATI.DeviceDesc0% = ATISMBus64, PCI\VEN_1002&DEV_4363 37 | %ATI.DeviceDesc0% = ATISMBus64, PCI\VEN_1002&DEV_4372 38 | %ATI.DeviceDesc0% = ATISMBus64, PCI\VEN_1002&DEV_4385 39 | 40 | ; 41 | ; General installation section 42 | ; 43 | 44 | [ATISMBus] 45 | AddReg=Install.AddReg 46 | 47 | [ATISMBus64] 48 | AddReg=Install.AddReg.NTamd64 49 | 50 | ; 51 | ; Service Installation 52 | ; 53 | 54 | [ATISMBus.Services] 55 | AddService = , 0x00000002 56 | 57 | [ATISMBus64.Services] 58 | AddService = , 0x00000002 59 | 60 | [ATISMBus_Service_Inst] 61 | ServiceType = 1 ; SERVICE_KERNEL_DRIVER 62 | StartType = 3 ; SERVICE_DEMAND_START 63 | ErrorControl = 0 ; SERVICE_ERROR_IGNORE 64 | LoadOrderGroup = Pointer Port 65 | 66 | [ATISMBus_EventLog_Inst] 67 | AddReg = ATISMBus_EventLog_AddReg 68 | 69 | [ATISMBus_EventLog_AddReg] 70 | 71 | [Install.AddReg] 72 | HKLM,"Software\ATI Technologies\Install\South Bridge\SMBus",DisplayName,,"ATI SMBus" 73 | HKLM,"Software\ATI Technologies\Install\South Bridge\SMBus",Version,,"5.10.1000.8" 74 | HKLM,"Software\ATI Technologies\Install\South Bridge\SMBus",Install,,"Success" 75 | 76 | [Install.AddReg.NTamd64] 77 | HKLM,"Software\Wow6432Node\ATI Technologies\Install\South Bridge\SMBus",DisplayName,,"ATI SMBus" 78 | HKLM,"Software\Wow6432Node\ATI Technologies\Install\South Bridge\SMBus",Version,,"5.10.1000.8" 79 | HKLM,"Software\Wow6432Node\ATI Technologies\Install\South Bridge\SMBus",Install,,"Success" 80 | 81 | ; 82 | ; Source file information 83 | ; 84 | 85 | [SourceDisksNames] 86 | 1 = %DiskId1%,,, 87 | 88 | [SourceDisksFiles] 89 | ; Files for disk ATI Technologies Inc Installation Disk #1 (System) 90 | 91 | [Strings] 92 | 93 | ; 94 | ; Non-Localizable Strings 95 | ; 96 | 97 | REG_SZ = 0x00000000 98 | REG_MULTI_SZ = 0x00010000 99 | REG_EXPAND_SZ = 0x00020000 100 | REG_BINARY = 0x00000001 101 | REG_DWORD = 0x00010001 102 | SERVICEROOT = "System\CurrentControlSet\Services" 103 | 104 | ; 105 | ; Localizable Strings 106 | ; 107 | 108 | ATI.DeviceDesc0 = "ATI SMBus" 109 | DiskId1 = "ATI Technologies Inc Installation Disk #1 (System)" 110 | ATI = "ATI Technologies Inc" 111 | -------------------------------------------------------------------------------- /OPSI/Backend/Manager/Authentication/PAM.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | PAM authentication. 8 | """ 9 | 10 | import grp 11 | import os 12 | import pwd 13 | from typing import Set 14 | 15 | import pam 16 | from opsicommon.logging import get_logger 17 | 18 | from OPSI.Backend.Manager.Authentication import AuthenticationModule 19 | from OPSI.Exceptions import BackendAuthenticationError 20 | from OPSI.System.Posix import isCentOS, isOpenSUSE, isRHEL, isSLES 21 | 22 | logger = get_logger("opsi.general") 23 | 24 | 25 | class PAMAuthentication(AuthenticationModule): 26 | def __init__(self, pam_service: str = None): 27 | super().__init__() 28 | self._pam_service = pam_service 29 | if not self._pam_service: 30 | if os.path.exists("/etc/pam.d/opsi-auth"): 31 | # Prefering our own - if present. 32 | self._pam_service = "opsi-auth" 33 | elif isSLES() or isOpenSUSE(): 34 | self._pam_service = "sshd" 35 | elif isCentOS() or isRHEL(): 36 | self._pam_service = "system-auth" 37 | else: 38 | self._pam_service = "common-auth" 39 | 40 | def get_instance(self): 41 | return PAMAuthentication(self._pam_service) 42 | 43 | def authenticate(self, username: str, password: str) -> None: 44 | """ 45 | Authenticate a user by PAM (Pluggable Authentication Modules). 46 | Important: the uid running this code needs access to /etc/shadow 47 | if os uses traditional unix authentication mechanisms. 48 | 49 | :param service: The PAM service to use. Leave None for autodetection. 50 | :type service: str 51 | :raises BackendAuthenticationError: If authentication fails. 52 | """ 53 | logger.confidential( 54 | "Trying to authenticate user %s with password %s by PAM", username, password 55 | ) 56 | logger.trace( 57 | "Attempting PAM authentication as user %s (service=%s)...", 58 | username, 59 | self._pam_service, 60 | ) 61 | 62 | try: 63 | auth = pam.pam() 64 | if not auth.authenticate(username, password, service=self._pam_service): 65 | logger.trace( 66 | "PAM authentication failed: %s (code %s)", auth.reason, auth.code 67 | ) 68 | raise RuntimeError(auth.reason) 69 | 70 | logger.trace("PAM authentication successful.") 71 | except Exception as err: 72 | raise BackendAuthenticationError( 73 | f"PAM authentication failed for user '{username}': {err}" 74 | ) from err 75 | 76 | def get_groupnames(self, username: str) -> Set[str]: 77 | """ 78 | Read the groups of a user. 79 | 80 | :returns: Group the user is a member of. 81 | :rtype: set() 82 | """ 83 | logger.debug("Getting groups of user %s", username) 84 | primary_gid = pwd.getpwnam(username).pw_gid 85 | logger.debug("Primary group id of user %s is %s", username, primary_gid) 86 | groups = set() 87 | for gid in os.getgrouplist(username, primary_gid): 88 | try: 89 | groups.add(grp.getgrgid(gid).gr_name) 90 | except KeyError as err: 91 | logger.warning(err) 92 | logger.debug("User %s is member of groups: %s", username, groups) 93 | return {g.lower() for g in groups} 94 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Helpers for testing opsi. 8 | """ 9 | 10 | import os 11 | import shutil 12 | import tempfile 13 | from contextlib import contextmanager 14 | from unittest import mock 15 | 16 | from OPSI.Util.Path import cd 17 | 18 | 19 | @contextmanager 20 | def workInTemporaryDirectory(tempDir=None): 21 | """ 22 | Creates a temporary folder to work in. Deletes the folder afterwards. 23 | 24 | :param tempDir: use the given dir as temporary directory. Will not \ 25 | be deleted if given. 26 | """ 27 | temporary_folder = tempDir or tempfile.mkdtemp() 28 | with cd(temporary_folder): 29 | try: 30 | yield temporary_folder 31 | finally: 32 | if not tempDir: 33 | try: 34 | shutil.rmtree(temporary_folder) 35 | except OSError: 36 | pass 37 | 38 | 39 | @contextmanager 40 | def createTemporaryTestfile(original, tempDir=None): 41 | """Copy `original` to a temporary directory and \ 42 | yield the path to the new file. 43 | 44 | The temporary directory can be specified overridden with `tempDir`.""" 45 | 46 | with workInTemporaryDirectory(tempDir) as targetDir: 47 | shutil.copy(original, targetDir) 48 | 49 | filename = os.path.basename(original) 50 | 51 | yield os.path.join(targetDir, filename) 52 | 53 | 54 | def getLocalFQDN(): 55 | "Get the FQDN of the local machine." 56 | # Lazy imports to not hinder other tests. 57 | from OPSI.Types import forceHostId 58 | from OPSI.Util import getfqdn 59 | 60 | return forceHostId(getfqdn()) 61 | 62 | 63 | @contextmanager 64 | def patchAddress(fqdn="opsi.test.invalid", address="172.16.0.1"): 65 | """ 66 | Modify the results of socket so that expected addresses are returned. 67 | 68 | :param fqdn: The FQDN to use. Everything before the first '.' will serve\ 69 | as hostname. 70 | :param address: The IP address to use. 71 | """ 72 | hostname = fqdn.split(".")[0] 73 | 74 | def getfqdn(*_): 75 | return fqdn 76 | 77 | def gethostbyaddr(*_): 78 | return (fqdn, [hostname], [address]) 79 | 80 | with mock.patch("socket.getfqdn", getfqdn): 81 | with mock.patch("socket.gethostbyaddr", gethostbyaddr): 82 | yield 83 | 84 | 85 | @contextmanager 86 | def patchEnvironmentVariables(**environmentVariables): 87 | """ 88 | Patches to environment variables to be empty during the context. 89 | Anything supplied as keyword argument will be added to the environment. 90 | """ 91 | originalEnv = os.environ.copy() 92 | try: 93 | os.environ.clear() 94 | for key, value in environmentVariables.items(): 95 | os.environ[key] = value 96 | 97 | yield 98 | finally: 99 | os.environ = originalEnv 100 | 101 | 102 | @contextmanager 103 | def fakeGlobalConf(fqdn="opsi.test.invalid", dir=None): 104 | "Fake a global.conf and return the path to the file." 105 | 106 | with workInTemporaryDirectory(dir) as tempDir: 107 | configPath = os.path.join(tempDir, "global.conf") 108 | 109 | with open(configPath, "w", encoding="utf-8") as conf: 110 | conf.write("[global]\n") 111 | conf.write(f"hostname = {fqdn}\n") 112 | yield configPath 113 | -------------------------------------------------------------------------------- /tests/test_util_file_dhcpdconf.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing the work with the DHCPD configuration files. 8 | """ 9 | 10 | import codecs 11 | import os 12 | 13 | import pytest 14 | 15 | from OPSI.Util.File import DHCPDConfFile 16 | 17 | from .helpers import createTemporaryTestfile 18 | 19 | 20 | def testParsingExampleDHCPDConf(test_data_path): 21 | testExample = os.path.join(test_data_path, "util", "dhcpd", "dhcpd_1.conf") 22 | 23 | with createTemporaryTestfile(testExample) as fileName: 24 | confFile = DHCPDConfFile(fileName) 25 | confFile.parse() 26 | 27 | 28 | @pytest.fixture 29 | def dhcpdConf(tempDir): 30 | """Mixin for an DHCPD backend. 31 | Manages a subnet 192.168.99.0/24""" 32 | 33 | testData = """ 34 | ddns-update-style none; 35 | default-lease-time 68400; 36 | # max-lease-time 68400; 37 | max-lease-time 68400; 38 | authoritative ; 39 | log-facility local7; 40 | use-host-decl-names on; 41 | option domain-name "domain.local"; 42 | option domain-name-servers ns.domain.local; 43 | option routers 192.168.99.254; 44 | 45 | # Comment netbios name servers 46 | option netbios-name-servers 192.168.99.2; 47 | 48 | subnet 192.168.99.0 netmask 255.255.255.0 { 49 | group { 50 | # Opsi hosts 51 | next-server 192.168.99.2; 52 | filename "linux/pxelinux.0/xxx?{}"; 53 | host opsi-test { 54 | hardware ethernet 9a:e5:3c:10:22:22; 55 | fixed-address opsi-test.domain.local; 56 | } 57 | } 58 | host out-of-group { 59 | hardware ethernet 9a:e5:3c:10:22:22; 60 | fixed-address out-of-group.domain.local; 61 | } 62 | } 63 | host out-of-subnet { 64 | hardware ethernet 1a:25:31:11:23:21; 65 | fixed-address out-of-subnet.domain.local; 66 | } 67 | """ 68 | 69 | dhcpdConfFile = os.path.join(tempDir, "dhcpd.conf") 70 | 71 | with codecs.open(dhcpdConfFile, "w", "utf-8") as f: 72 | f.write(testData) 73 | 74 | yield DHCPDConfFile(dhcpdConfFile) 75 | 76 | 77 | def testAddingHostsToConfig(dhcpdConf): 78 | """ 79 | Adding hosts to a DHCPDConf. 80 | 81 | If this fails on your machine with a message that 127.x.x.x is refused 82 | as network address please correct your hostname settings. 83 | """ 84 | dhcpdConf.parse() 85 | 86 | dhcpdConf.addHost( 87 | "TestclienT", "0001-21-21:00:00", "192.168.99.112", "192.168.99.112", None 88 | ) 89 | dhcpdConf.addHost( 90 | "TestclienT2", 91 | "00:01:09:08:99:11", 92 | "192.168.99.113", 93 | "192.168.99.113", 94 | {"next-server": "192.168.99.2", "filename": "linux/pxelinux.0/xxx?{}"}, 95 | ) 96 | 97 | assert dhcpdConf.getHost("TestclienT2") is not None 98 | assert dhcpdConf.getHost("notthere") is None 99 | 100 | 101 | def testGeneratingConfig(dhcpdConf): 102 | dhcpdConf.parse() 103 | 104 | dhcpdConf.addHost( 105 | "TestclienT", "0001-21-21:00:00", "192.168.99.112", "192.168.99.112", None 106 | ) 107 | dhcpdConf.addHost( 108 | "TestclienT2", 109 | "00:01:09:08:99:11", 110 | "192.168.99.113", 111 | "192.168.99.113", 112 | {"next-server": "192.168.99.2", "filename": "linux/pxelinux.0/xxx?{}"}, 113 | ) 114 | 115 | dhcpdConf.generate() 116 | # TODO: check generated file 117 | -------------------------------------------------------------------------------- /tests/test_util_task_backup.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing the task to backup opsi. 8 | """ 9 | 10 | import os 11 | import shutil 12 | import sys 13 | 14 | from OPSI.Util.Task.Backup import OpsiBackup 15 | from OPSI.Util.Task.ConfigureBackend import getBackendConfiguration, updateConfigFile 16 | 17 | from .helpers import mock, workInTemporaryDirectory 18 | 19 | try: 20 | from .Backends.MySQL import MySQLconfiguration 21 | except ImportError: 22 | MySQLconfiguration = None 23 | 24 | 25 | def testVerifySysConfigDoesNotFailBecauseWhitespaceAtEnd(): 26 | backup = OpsiBackup() 27 | 28 | archive = { 29 | "distribution": "SUSE Linux Enterprise Server", 30 | "sysVersion": "(12, 0)", 31 | } 32 | system = { 33 | "distribution": "SUSE Linux Enterprise Server ", # note the extra space 34 | "sysVersion": (12, 0), 35 | } 36 | 37 | assert {} == backup.getDifferencesInSysConfig(archive, sysInfo=system) 38 | 39 | 40 | def testPatchingStdout(): 41 | fake = "fake" 42 | backup = OpsiBackup(stdout=fake) 43 | assert fake == backup.stdout 44 | 45 | newBackup = OpsiBackup() 46 | assert sys.stdout == newBackup.stdout 47 | 48 | 49 | def testGettingArchive(dist_data_path): 50 | fakeBackendDir = os.path.join(dist_data_path, "backends") 51 | fakeBackendDir = os.path.normpath(fakeBackendDir) 52 | 53 | with mock.patch( 54 | "OPSI.Util.Task.Backup.OpsiBackupArchive.BACKEND_CONF_DIR", fakeBackendDir 55 | ): 56 | backup = OpsiBackup() 57 | archive = backup._getArchive("r") 58 | 59 | assert os.path.exists(archive.name), "No archive created." 60 | os.remove(archive.name) 61 | 62 | 63 | def testCreatingArchive(dist_data_path): 64 | with workInTemporaryDirectory() as backendDir: 65 | with workInTemporaryDirectory() as tempDir: 66 | assert 0 == len(os.listdir(tempDir)), "Directory not empty" 67 | 68 | configDir = os.path.join(backendDir, "config") 69 | os.mkdir(configDir) 70 | 71 | sourceBackendDir = os.path.join(dist_data_path, "backends") 72 | sourceBackendDir = os.path.normpath(sourceBackendDir) 73 | fakeBackendDir = os.path.join(backendDir, "backends") 74 | 75 | shutil.copytree(sourceBackendDir, fakeBackendDir) 76 | 77 | for filename in os.listdir(fakeBackendDir): 78 | if not filename.endswith(".conf"): 79 | continue 80 | 81 | configPath = os.path.join(fakeBackendDir, filename) 82 | config = getBackendConfiguration(configPath) 83 | if "file" in filename: 84 | config["baseDir"] = configDir 85 | elif "mysql" in filename and MySQLconfiguration: 86 | config.update(MySQLconfiguration) 87 | else: 88 | continue # no modifications here 89 | 90 | updateConfigFile(configPath, config) 91 | 92 | with mock.patch( 93 | "OPSI.Util.Task.Backup.OpsiBackupArchive.CONF_DIR", 94 | os.path.dirname(__file__), 95 | ): 96 | with mock.patch( 97 | "OPSI.Util.Task.Backup.OpsiBackupArchive.BACKEND_CONF_DIR", 98 | fakeBackendDir, 99 | ): 100 | backup = OpsiBackup() 101 | backup.create() 102 | 103 | dirListing = os.listdir(tempDir) 104 | try: 105 | dirListing.remove(".coverage") 106 | except ValueError: 107 | pass 108 | 109 | assert len(dirListing) == 1 110 | -------------------------------------------------------------------------------- /tests/data/backend/small_extended_hwaudit.conf: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | global OPSI_HARDWARE_CLASSES 4 | OPSI_HARDWARE_CLASSES = [ 5 | { 6 | "Class": { 7 | "Type": "VIRTUAL", 8 | "Opsi": "BASIC_INFO" 9 | }, 10 | "Values": [ 11 | { 12 | "Type": "varchar(100)", 13 | "Scope": "g", 14 | "Opsi": "name", 15 | "WMI": "Name", 16 | "Linux": "product" 17 | }, 18 | { 19 | "Type": "varchar(100)", 20 | "Scope": "g", 21 | "Opsi": "description", 22 | "WMI": "Description", 23 | "Linux": "description" 24 | }, 25 | #{ 26 | # "Type": "varchar(60)", 27 | # "Scope": "g", 28 | # "Opsi": "caption", 29 | # "WMI": "Caption" 30 | #} 31 | ] 32 | }, 33 | { 34 | "Class": { 35 | "Type": "VIRTUAL", 36 | "Super": [ "BASIC_INFO" ], 37 | "Opsi": "HARDWARE_DEVICE" 38 | }, 39 | "Values": [ 40 | { 41 | "Type": "varchar(50)", 42 | "Scope": "g", 43 | "Opsi": "vendor", 44 | "WMI": "Manufacturer", 45 | "Linux": "vendor" 46 | }, 47 | { 48 | "Type": "varchar(100)", 49 | "Scope": "g", 50 | "Opsi": "model", 51 | "WMI": "Model", 52 | "Linux": "product" 53 | }, 54 | { 55 | "Type": "varchar(50)", 56 | "Scope": "i", 57 | "Opsi": "serialNumber", 58 | "WMI": "SerialNumber", 59 | "Linux": "serial" 60 | }, 61 | ] 62 | }, 63 | { 64 | "Class": { 65 | "Type": "STRUCTURAL", 66 | "Super": [ "HARDWARE_DEVICE" ], 67 | "Opsi": "COMPUTER_SYSTEM", 68 | "WMI": "select * from Win32_ComputerSystem", 69 | "Linux": "[lshw]system" 70 | }, 71 | "Values": [ 72 | { 73 | "Type": "varchar(100)", 74 | "Scope": "i", 75 | "Opsi": "name", 76 | "WMI": "Name", 77 | "Linux": "id" 78 | }, 79 | { 80 | "Type": "varchar(50)", 81 | "Scope": "i", 82 | "Opsi": "systemType", 83 | "WMI": "SystemType", 84 | "Linux": "configuration/chassis" 85 | }, 86 | { 87 | "Type": "bigint", 88 | "Scope": "i", 89 | "Opsi": "totalPhysicalMemory", 90 | "WMI": "TotalPhysicalMemory", 91 | "Linux": "core/memory/size", 92 | "Unit": "Byte" 93 | }, 94 | { 95 | "Type": "varchar(255)", 96 | "Scope": "i", 97 | "Opsi": "sku", 98 | "Registry": "[HKEY_LOCAL_MACHINE\\HARDWARE\\DESCRIPTION\\System\\BIOS]SystemSKU", 99 | "Linux": "configuration/sku" 100 | }, 101 | { 102 | "Type": "varchar(50)", 103 | "Scope": "i", 104 | "Opsi": "dellexpresscode", 105 | "Condition": "vendor=[dD]ell*", 106 | "Cmd": "#dellexpresscode\\dellexpresscode.exe#.split('=')[1]", 107 | "Python": "str(int(#{'COMPUTER_SYSTEM':'serialNumber','CHASSIS':'serialNumber'}#,36))" 108 | } 109 | ] 110 | }, 111 | ] 112 | -------------------------------------------------------------------------------- /data/backendManager/acl.conf: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # = = = = = = = = = = = = = = = = = = = = 4 | # = backend acl configuration = 5 | # = = = = = = = = = = = = = = = = = = = = 6 | # 7 | # This file configures access control to protected backend methods. 8 | # Entries has to follow the form: 9 | # : 10 | # 11 | # acl enrties are specified like: 12 | # [([,attributes()])] 13 | # 14 | # For every method the first entry which allows (partial) access is decisive. 15 | # 16 | # Possible types of entries are: 17 | # all : everyone 18 | # sys_user : a system user 19 | # sys_group : a system group (possible placeholders are {admingroup} and {fileadmingroup}) 20 | # opsi_depotserver : an opsi depot server 21 | # opsi_client : an opsi client 22 | # self : the object to be read or written 23 | # 24 | # Examples: 25 | # host_getObjects : self 26 | # allow clients to read their own host objects 27 | # host_deleteObjects : sys_user(admin,opsiadmin),sys_group(opsiadmins) 28 | # allow system users "admin", "opsiadmin" and members of system group "opsiadmins" to delete hosts 29 | # product_.* : opsi_client(client1.uib.local),opsi_depotserver 30 | # allow access to product objects to opsi client "client1.uib.local" and all opsi depot servers 31 | # host_getObjects : sys_user(user1,attributes(id,description,notes)) 32 | # allow partial access to host objects to system user "user1". "user1" is allowed to read object attributes "id", "description", "notes" 33 | # host_getObjects : sys_group(group1,attributes(!opsiHostKey)) 34 | # allow partial access to host objects to members of system group "group1". Members are allowed to read all object attributes except "opsiHostKey" 35 | 36 | backend_deleteBase : sys_group({admingroup}) 37 | backend_.* : all 38 | log_.* : sys_group({admingroup}); opsi_depotserver; self 39 | hostControl.* : sys_group({admingroup}); opsi_depotserver 40 | host_get.* : sys_group({admingroup}); opsi_depotserver; self; opsi_client(attributes(!opsiHostKey,!description,!lastSeen,!notes,!hardwareAddress,!inventoryNumber)) 41 | host_update.* : sys_group({admingroup}); opsi_depotserver; self 42 | config_create.* : sys_group({admingroup}); opsi_depotserver; opsi_client 43 | auditSoftware_delete.* : sys_group({admingroup}); opsi_depotserver 44 | auditSoftware_.* : sys_group({admingroup}); opsi_depotserver; opsi_client 45 | auditHardware_delete.* : sys_group({admingroup}); opsi_depotserver 46 | auditHardware_.* : sys_group({admingroup}); opsi_depotserver; opsi_client 47 | user_setCredentials : sys_group({admingroup}); opsi_depotserver 48 | user_getCredentials : sys_group({admingroup}); opsi_depotserver; opsi_client 49 | auditHardwareOnHost_.* : sys_group({admingroup}); opsi_depotserver; self 50 | auditSoftwareOnClient_.* : sys_group({admingroup}); opsi_depotserver; self 51 | licenseOnClient_.* : sys_group({admingroup}); opsi_depotserver; self 52 | productOnClient_.* : sys_group({admingroup}); opsi_depotserver; self 53 | configState_.* : sys_group({admingroup}); opsi_depotserver; self 54 | .*_get.* : sys_group({admingroup}); opsi_depotserver; opsi_client 55 | productPropertyState_.* : sys_group({admingroup}); opsi_depotserver; self 56 | get(Raw){0,1}Data : sys_group({admingroup}); opsi_depotserver 57 | .* : sys_group({admingroup}); opsi_depotserver 58 | -------------------------------------------------------------------------------- /OPSI/Backend/Manager/Authentication/NT.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | PAM authentication. 8 | """ 9 | 10 | from typing import Set 11 | 12 | # pyright: reportMissingImports=false 13 | import win32net 14 | import win32security 15 | from opsicommon.logging import get_logger 16 | 17 | from OPSI.Backend.Manager.Authentication import AuthenticationModule 18 | from OPSI.Config import OPSI_ADMIN_GROUP 19 | from OPSI.Exceptions import BackendAuthenticationError 20 | 21 | logger = get_logger("opsi.general") 22 | 23 | 24 | class NTAuthentication(AuthenticationModule): 25 | def __init__(self, admin_group_sid: str = None): 26 | super().__init__() 27 | self._admin_group_sid = admin_group_sid 28 | self._admin_groupname = OPSI_ADMIN_GROUP 29 | if self._admin_group_sid is not None: 30 | try: 31 | self._admin_groupname = win32security.LookupAccountSid( 32 | None, win32security.ConvertStringSidToSid(self._admin_group_sid) 33 | )[0] 34 | except Exception as err: 35 | logger.error( 36 | "Failed to lookup group with sid '%s': %s", 37 | self._admin_group_sid, 38 | err, 39 | ) 40 | 41 | def get_instance(self): 42 | return NTAuthentication(self._admin_group_sid) 43 | 44 | def authenticate(self, username: str, password: str) -> None: 45 | """ 46 | Authenticate a user by Windows-Login on current machine 47 | 48 | :raises BackendAuthenticationError: If authentication fails. 49 | """ 50 | logger.confidential( 51 | "Trying to authenticate user %s with password %s by win32security", 52 | username, 53 | password, 54 | ) 55 | 56 | try: 57 | win32security.LogonUser( 58 | username, 59 | "None", 60 | password, 61 | win32security.LOGON32_LOGON_NETWORK, 62 | win32security.LOGON32_PROVIDER_DEFAULT, 63 | ) 64 | except Exception as err: 65 | raise BackendAuthenticationError( 66 | f"Win32security authentication failed for user '{username}': {err}" 67 | ) from err 68 | 69 | def get_admin_groupname(self) -> str: 70 | return self._admin_groupname.lower() 71 | 72 | def get_groupnames(self, username: str) -> Set[str]: 73 | """ 74 | Read the groups of a user. 75 | 76 | :returns: List og group names the user is a member of. 77 | :rtype: set() 78 | """ 79 | collected_groupnames = set() 80 | 81 | gresume = 0 82 | while True: 83 | (groups, gtotal, gresume) = win32net.NetLocalGroupEnum(None, 0, gresume) 84 | logger.trace( 85 | "Got %s groups, total=%s, resume=%s", len(groups), gtotal, gresume 86 | ) 87 | for groupname in (u["name"] for u in groups): 88 | logger.trace("Found group '%s'", groupname) 89 | uresume = 0 90 | while True: 91 | (users, utotal, uresume) = win32net.NetLocalGroupGetMembers( 92 | None, groupname, 0, uresume 93 | ) 94 | logger.trace( 95 | "Got %s users, total=%s, resume=%s", len(users), utotal, uresume 96 | ) 97 | for sid in (u["sid"] for u in users): 98 | (group_username, _domain, _group_type) = ( 99 | win32security.LookupAccountSid(None, sid) 100 | ) 101 | if group_username.lower() == username.lower(): 102 | collected_groupnames.add(groupname) 103 | logger.debug( 104 | "User %s is member of group %s", username, groupname 105 | ) 106 | if uresume == 0: 107 | break 108 | if gresume == 0: 109 | break 110 | 111 | return {g.lower() for g in collected_groupnames} 112 | -------------------------------------------------------------------------------- /OPSI/Util/File/Opsi/Opsirc.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Handling an .opsirc file. 8 | 9 | An .opsirc file contains information about how to connect to an 10 | opsi API. 11 | By default the file is expected to be at `~/.opsi.org/opsirc`. 12 | 13 | An example:: 14 | 15 | address = https://opsimain.domain.local:4447/rpc 16 | username = myname 17 | password = topsecret 18 | 19 | 20 | None of these settings are mandatory. 21 | 22 | Instead of writing the password directly to the file it is possible 23 | to reference a file with the secret as follows:: 24 | 25 | password file = ~/.opsi.org/opsirc.secret 26 | 27 | 28 | The files should be encoded as utf-8. 29 | """ 30 | 31 | import codecs 32 | import os 33 | 34 | from opsicommon.logging import get_logger, secret_filter 35 | 36 | from OPSI.Types import forceUnicode, forceUrl 37 | 38 | __all__ = ("getOpsircPath", "readOpsirc") 39 | 40 | logger = get_logger("opsi.general") 41 | 42 | 43 | def readOpsirc(filename=None): 44 | """ 45 | Read the configuration file and parse it for usable information. 46 | 47 | :param filename: The path of the file to read. Defaults to using \ 48 | the result from `getOpsircPath`. 49 | :type filename: str 50 | :returns: Settings read from the file. Possible keys are `username`,\ 51 | `password` and `address`. 52 | :rtype: {str: str} 53 | """ 54 | if filename is None: 55 | filename = getOpsircPath() 56 | 57 | if not os.path.exists(filename): 58 | logger.debug(".opsirc file %s does not exist.", filename) 59 | return {} 60 | 61 | return _parseConfig(filename) 62 | 63 | 64 | def getOpsircPath(): 65 | """ 66 | Return the path where an opsirc file is expected to be. 67 | 68 | :return: The path of an opsirc file. 69 | :rtype: str 70 | """ 71 | path = os.path.expanduser("~/.opsi.org/opsirc") 72 | return path 73 | 74 | 75 | def _parseConfig(filename): 76 | config = {} 77 | with codecs.open(filename, mode="r", encoding="utf-8") as opsircfile: 78 | for line in opsircfile: 79 | line = line.strip() 80 | if line.startswith(("#", ";")) or not line: 81 | continue 82 | 83 | try: 84 | key, value = line.split("=", 1) 85 | except ValueError: 86 | logger.trace("Unable to split line %s", line) 87 | continue 88 | 89 | key = key.strip() 90 | value = value.strip() 91 | 92 | if not value: 93 | logger.warning( 94 | "There is no value for %s in opsirc file %s, skipping.", 95 | key, 96 | filename, 97 | ) 98 | continue 99 | 100 | if key == "address": 101 | config[key] = forceUrl(value) 102 | elif key == "username": 103 | config[key] = forceUnicode(value) 104 | elif key == "password": 105 | value = forceUnicode(value) 106 | secret_filter.add_secrets(value) 107 | config[key] = value 108 | elif key == "password file": 109 | passwordFilePath = os.path.expanduser(value) 110 | value = _readPasswordFile(passwordFilePath) 111 | secret_filter.add_secrets(value) 112 | config["password"] = value 113 | else: 114 | logger.debug("Ignoring unknown key %s", key) 115 | 116 | logger.debug( 117 | "Found the following usable keys in %s: %s", 118 | filename, 119 | ", ".join(list(config.keys())), 120 | ) 121 | return config 122 | 123 | 124 | def _readPasswordFile(filename): 125 | with codecs.open(filename, mode="r", encoding="utf-8") as pwfile: 126 | password = pwfile.read() 127 | 128 | return password.strip() 129 | -------------------------------------------------------------------------------- /OPSI/Backend/JSONRPC.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | JSONRPC backend. 8 | 9 | This backend executes the calls on a remote backend via JSONRPC. 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | from threading import Event 15 | from typing import Any 16 | from urllib.parse import urlparse 17 | 18 | from opsicommon.client.opsiservice import ServiceClient, ServiceConnectionListener 19 | from opsicommon.logging import get_logger 20 | 21 | from OPSI import __version__ 22 | from OPSI.Backend.Base import Backend 23 | 24 | logger = get_logger("opsi.general") 25 | 26 | __all__ = ("JSONRPCBackend",) 27 | 28 | 29 | class JSONRPCBackend(Backend, ServiceConnectionListener): 30 | """ 31 | This Backend gives remote access to a Backend reachable via jsonrpc. 32 | """ 33 | 34 | def __init__(self, address: str, **kwargs: Any) -> None: 35 | """ 36 | Backend for JSON-RPC access to another opsi service. 37 | 38 | :param compression: Should requests be compressed? 39 | :type compression: bool 40 | """ 41 | 42 | self._name = "jsonrpc" 43 | self._connection_result_event = Event() 44 | self._connection_error: Exception | None = None 45 | 46 | Backend.__init__(self, **kwargs) # type: ignore[misc] 47 | 48 | connect_on_init = True 49 | service_args = { 50 | "address": address, 51 | "user_agent": f"opsi-jsonrpc-backend/{__version__}", 52 | "verify": "accept_all", 53 | "jsonrpc_create_objects": True, 54 | "jsonrpc_create_methods": True, 55 | } 56 | for option, value in kwargs.items(): 57 | option = option.lower().replace("_", "") 58 | if option == "username": 59 | service_args["username"] = str(value or "") 60 | elif option == "password": 61 | service_args["password"] = str(value or "") 62 | elif option == "cacertfile": 63 | if value not in (None, ""): 64 | service_args["ca_cert_file"] = str(value) 65 | elif option == "verifyservercert": 66 | if value: 67 | service_args["verify"] = ["opsi_ca", "uib_opsi_ca"] 68 | else: 69 | service_args["verify"] = "accept_all" 70 | elif option == "verify": 71 | service_args["verify"] = value 72 | elif option == "sessionid": 73 | if value: 74 | service_args["session_cookie"] = str(value) 75 | elif option == "sessionlifetime": 76 | if value: 77 | service_args["session_lifetime"] = int(value) 78 | elif option == "proxyurl": 79 | service_args["proxy_url"] = str(value) if value else None 80 | elif option == "application": 81 | service_args["user_agent"] = str(value) 82 | elif option == "connecttimeout": 83 | service_args["connect_timeout"] = int(value) 84 | elif option == "connectoninit": 85 | connect_on_init = bool(value) 86 | 87 | self.service = ServiceClient(**service_args) 88 | self.service.register_connection_listener(self) 89 | if connect_on_init: 90 | self.service.connect() 91 | self._connection_result_event.wait() 92 | if self._connection_error: 93 | raise self._connection_error 94 | 95 | @property 96 | def hostname(self) -> str: 97 | return urlparse(self.service.base_url).hostname 98 | 99 | def connection_established(self, service_client: ServiceClient) -> None: 100 | self.service.create_jsonrpc_methods(self) 101 | self._connection_error = None 102 | self._connection_result_event.set() 103 | 104 | def connection_failed( 105 | self, service_client: ServiceClient, exception: Exception 106 | ) -> None: 107 | self._connection_error = exception 108 | self._connection_result_event.set() 109 | -------------------------------------------------------------------------------- /tests/test_util_file_opsi_opsirc.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing .opsirc handling. 8 | """ 9 | 10 | import codecs 11 | import os 12 | import pytest 13 | 14 | from OPSI.Util.File.Opsi.Opsirc import getOpsircPath, readOpsirc 15 | from OPSI.Util import randomString 16 | 17 | 18 | @pytest.fixture 19 | def filename(tempDir): 20 | return os.path.join(tempDir, randomString(8) + ".conf") 21 | 22 | 23 | def testReadingMissingFileReturnsNoConfig(tempDir): 24 | assert {} == readOpsirc("iamnothere") 25 | 26 | 27 | def testReadingEmptyConfigFile(filename): 28 | with open(filename, "w"): 29 | pass 30 | 31 | config = readOpsirc(filename) 32 | 33 | assert {} == config 34 | 35 | 36 | def testReadingConfigFile(filename): 37 | with open(filename, "w") as f: 38 | f.write("address = https://lullaby.machine.dream:12345/c3\n") 39 | f.write("username = hanz\n") 40 | f.write("password = gr3tel\n") 41 | 42 | config = readOpsirc(filename) 43 | 44 | assert len(config) == 3 45 | assert config["address"] == "https://lullaby.machine.dream:12345/c3" 46 | assert config["username"] == "hanz" 47 | assert config["password"] == "gr3tel" 48 | 49 | 50 | def testReadingConfigFileIgnoresLeadingAndTrailingSpacing(filename): 51 | with open(filename, "w") as f: 52 | f.write(" address = https://lullaby.machine.dream:12345/c3\n") 53 | f.write("username=hanz \n") 54 | f.write(" password = gr3tel \n") 55 | 56 | config = readOpsirc(filename) 57 | 58 | assert len(config) == 3 59 | assert config["address"] == "https://lullaby.machine.dream:12345/c3" 60 | assert config["username"] == "hanz" 61 | assert config["password"] == "gr3tel" 62 | 63 | 64 | def testReadingPasswordFromCredentialsfile(filename): 65 | password = randomString(32) 66 | 67 | pwdfile = filename + ".secret" 68 | with codecs.open(pwdfile, "w", "utf-8") as f: 69 | f.write(password + "\n") 70 | 71 | with open(filename, "w") as f: 72 | f.write("address = https://lullaby.machine.dream:12345/c3\n") 73 | f.write("username = hanz\n") 74 | f.write("password file = {}\n".format(pwdfile)) 75 | 76 | config = readOpsirc(filename) 77 | 78 | assert len(config) == 3 79 | assert config["address"] == "https://lullaby.machine.dream:12345/c3" 80 | assert config["username"] == "hanz" 81 | assert config["password"] == password 82 | 83 | 84 | def testIgnoringComments(filename): 85 | with open(filename, "w") as f: 86 | f.write(";address = https://bad.guy.dream:12345/c3\n") 87 | f.write("# address = https://blue.pill.dream:12345/c3\n") 88 | f.write("address = https://lullaby.machine.dream:12345/c3\n") 89 | f.write(" # address = https://last.one.neo:12345/c3\n") 90 | 91 | config = readOpsirc(filename) 92 | 93 | assert len(config) == 1 94 | assert config["address"] == "https://lullaby.machine.dream:12345/c3" 95 | 96 | 97 | def testIgnoringUnknownKeywords(filename): 98 | with open(filename, "w") as f: 99 | f.write("hello = world\n") 100 | f.write("i coded = a bot\n") 101 | f.write("and I liked it\n") 102 | 103 | config = readOpsirc(filename) 104 | 105 | assert not config 106 | 107 | 108 | def testIgnoringEmptyValues(filename): 109 | with open(filename, "w") as f: 110 | f.write("username=\n") 111 | f.write("username = foo\n") 112 | f.write("username = \n") 113 | 114 | config = readOpsirc(filename) 115 | 116 | assert len(config) == 1 117 | assert config["username"] == "foo" 118 | 119 | 120 | def testReadingOpsircPath(): 121 | path = getOpsircPath() 122 | assert "~" not in path 123 | 124 | head, tail = os.path.split(path) 125 | assert tail == "opsirc" 126 | assert head.endswith(".opsi.org") 127 | -------------------------------------------------------------------------------- /tests/test_backend_methods.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing unbound methods for the backends. 8 | """ 9 | 10 | from OPSI.Backend.Base.Extended import get_function_signature_and_args 11 | 12 | 13 | def test_getting_signature_for_method_without_arguments(): 14 | def foo(): 15 | pass 16 | 17 | sig, args = get_function_signature_and_args(foo) 18 | 19 | assert sig == "()" 20 | assert not args 21 | 22 | 23 | def test_getting_signature_for_method_with_one_positional_argument(): 24 | def foo(bar): 25 | pass 26 | 27 | sig, args = get_function_signature_and_args(foo) 28 | 29 | assert sig == "(bar)" 30 | assert args == "bar=bar" 31 | 32 | 33 | def test_getting_signature_for_method_with_multiple_positional_arguments(): 34 | def foo(bar, baz): 35 | pass 36 | 37 | sig, args = get_function_signature_and_args(foo) 38 | 39 | assert sig == "(bar, baz)" 40 | assert args == "bar=bar, baz=baz" 41 | 42 | 43 | def test_getting_signature_for_method_with_keyword_argument_only(): 44 | def foo(bar=None): 45 | pass 46 | 47 | sig, args = get_function_signature_and_args(foo) 48 | 49 | assert "(bar=None)" == sig 50 | assert "bar=bar" == args 51 | 52 | 53 | def test_getting_signature_for_method_with_multiple_keyword_arguments_only(): 54 | def foo(bar=None, baz=None): 55 | pass 56 | 57 | sig, args = get_function_signature_and_args(foo) 58 | 59 | assert sig == "(bar=None, baz=None)" 60 | assert args == "bar=bar, baz=baz" 61 | 62 | 63 | def test_getting_signature_for_method_with_mixed_arguments(): 64 | def foo(bar, baz=None): 65 | pass 66 | 67 | sig, args = get_function_signature_and_args(foo) 68 | 69 | assert sig == "(bar, baz=None)" 70 | assert args == "bar=bar, baz=baz" 71 | 72 | 73 | def test_self_as_first_argument_is_ignored(): 74 | def foo(self, bar=None): 75 | pass 76 | 77 | sig, args = get_function_signature_and_args(foo) 78 | 79 | assert sig == "(bar=None)" 80 | assert args == "bar=bar" 81 | 82 | 83 | def test_argument_with_string_default(): 84 | def foo(bar="baz"): 85 | pass 86 | 87 | sig, args = get_function_signature_and_args(foo) 88 | 89 | assert sig == "(bar='baz')" 90 | assert args == "bar=bar" 91 | 92 | 93 | def test_argument_with_variable_argument_count(): 94 | def foo(*bar): 95 | pass 96 | 97 | sig, args = get_function_signature_and_args(foo) 98 | 99 | assert sig == "(*bar)" 100 | assert args == "*bar" 101 | 102 | 103 | def test_argument_with_positional_argument_and_variable_argument_count(): 104 | def foo(bar, *baz): 105 | pass 106 | 107 | sig, args = get_function_signature_and_args(foo) 108 | 109 | assert sig == "(bar, *baz)" 110 | assert args == "bar=bar, *baz" 111 | 112 | 113 | def test_variable_keyword_arguments(): 114 | def foo(**bar): 115 | pass 116 | 117 | sig, args = get_function_signature_and_args(foo) 118 | 119 | assert sig == "(**bar)" 120 | assert args == "**bar" 121 | 122 | 123 | def test_method_with_all_types_of_arguments(): 124 | def foo(ironman, blackWidow=True, *hulk, **deadpool): 125 | pass 126 | 127 | sig, args = get_function_signature_and_args(foo) 128 | 129 | assert sig == "(ironman, blackWidow=True, *hulk, **deadpool)" 130 | assert args == "ironman=ironman, blackWidow=blackWidow, *hulk, **deadpool" 131 | 132 | 133 | def test_method_with_all_types_of_arguments_and_annotations(): 134 | def foo(self, ironman, blackWidow: bool = True, *hulk, **deadpool) -> int: 135 | return 1 136 | 137 | sig, args = get_function_signature_and_args(foo) 138 | 139 | assert sig == "(ironman, blackWidow: bool = True, *hulk, **deadpool) -> int" 140 | assert args == "ironman=ironman, blackWidow=blackWidow, *hulk, **deadpool" 141 | -------------------------------------------------------------------------------- /tests/test_backend_extend_d_10_opsi.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Tests for the dynamically loaded OPSI 3.x legacy methods. 8 | 9 | This tests what usually is found under 10 | ``/etc/opsi/backendManager/extend.de/10_opsi.conf``. 11 | """ 12 | 13 | from OPSI.Object import ( 14 | OpsiClient, 15 | LocalbootProduct, 16 | ProductOnClient, 17 | ProductDependency, 18 | OpsiDepotserver, 19 | ProductOnDepot, 20 | UnicodeConfig, 21 | ConfigState, 22 | ) 23 | 24 | import pytest 25 | 26 | 27 | @pytest.fixture 28 | def prefilledBackendManager(backendManager): 29 | fillBackend(backendManager) 30 | yield backendManager 31 | 32 | 33 | def fillBackend(backend): 34 | client, depot = createClientAndDepot(backend) 35 | 36 | firstProduct = LocalbootProduct("to_install", "1.0", "1.0") 37 | secondProduct = LocalbootProduct("already_installed", "1.0", "1.0") 38 | 39 | prodDependency = ProductDependency( 40 | productId=firstProduct.id, 41 | productVersion=firstProduct.productVersion, 42 | packageVersion=firstProduct.packageVersion, 43 | productAction="setup", 44 | requiredProductId=secondProduct.id, 45 | # requiredProductVersion=secondProduct.productVersion, 46 | # requiredPackageVersion=secondProduct.packageVersion, 47 | requiredAction="setup", 48 | requiredInstallationStatus="installed", 49 | requirementType="after", 50 | ) 51 | 52 | backend.product_createObjects([firstProduct, secondProduct]) 53 | backend.productDependency_createObjects([prodDependency]) 54 | 55 | poc = ProductOnClient( 56 | clientId=client.id, 57 | productId=firstProduct.id, 58 | productType=firstProduct.getType(), 59 | productVersion=firstProduct.productVersion, 60 | packageVersion=firstProduct.packageVersion, 61 | installationStatus="installed", 62 | actionResult="successful", 63 | ) 64 | 65 | backend.productOnClient_createObjects([poc]) 66 | 67 | firstProductOnDepot = ProductOnDepot( 68 | productId=firstProduct.id, 69 | productType=firstProduct.getType(), 70 | productVersion=firstProduct.productVersion, 71 | packageVersion=firstProduct.packageVersion, 72 | depotId=depot.getId(), 73 | locked=False, 74 | ) 75 | 76 | secondProductOnDepot = ProductOnDepot( 77 | productId=secondProduct.id, 78 | productType=secondProduct.getType(), 79 | productVersion=secondProduct.productVersion, 80 | packageVersion=secondProduct.packageVersion, 81 | depotId=depot.getId(), 82 | locked=False, 83 | ) 84 | 85 | backend.productOnDepot_createObjects([firstProductOnDepot, secondProductOnDepot]) 86 | 87 | 88 | def createClientAndDepot(backend): 89 | client = OpsiClient( 90 | id="backend-test-1.vmnat.local", description="Unittest Test client." 91 | ) 92 | 93 | depot = OpsiDepotserver( 94 | id="depotserver1.some.test", 95 | description="Test Depot", 96 | ) 97 | 98 | backend.host_createObjects([client, depot]) 99 | 100 | clientConfigDepotId = UnicodeConfig( 101 | id="clientconfig.depot.id", 102 | description="Depotserver to use", 103 | possibleValues=[], 104 | defaultValues=[depot.id], 105 | ) 106 | 107 | backend.config_createObjects(clientConfigDepotId) 108 | 109 | clientDepotMappingConfigState = ConfigState( 110 | configId=clientConfigDepotId.getId(), 111 | objectId=client.getId(), 112 | values=depot.getId(), 113 | ) 114 | 115 | backend.configState_createObjects(clientDepotMappingConfigState) 116 | 117 | return client, depot 118 | 119 | 120 | def testBackendDoesNotCreateProductsOnClientsOnItsOwn(prefilledBackendManager): 121 | pocs = prefilledBackendManager.productOnClient_getObjects() 122 | assert 1 == len( 123 | pocs 124 | ), "Expected to have only one ProductOnClient but got {n} instead: {0}".format( 125 | pocs, n=len(pocs) 126 | ) 127 | -------------------------------------------------------------------------------- /OPSI/Util/Task/ConfigureBackend/__init__.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Functionality to automatically configure an OPSI backend. 8 | 9 | .. versionadded:: 4.0.4.6 10 | """ 11 | 12 | import codecs 13 | import os 14 | import re 15 | import socket 16 | import sys 17 | 18 | from opsicommon.logging import get_logger 19 | 20 | from OPSI.System.Posix import getLocalFqdn, getNetworkConfiguration 21 | from OPSI.Util import objectToBeautifiedText 22 | 23 | __all__ = ("getBackendConfiguration", "updateConfigFile") 24 | 25 | logger = get_logger("opsi.general") 26 | 27 | 28 | def getBackendConfiguration(backendConfigFile, customGlobals=None): 29 | """ 30 | Reads the backend configuration from the given file. 31 | 32 | :param backendConfigFile: Path to the backend configuration file. 33 | :param customGlobals: If special locals are needed for the config file \ 34 | please pass them here. If this is None defaults will be used. 35 | :type customGlobals: dict 36 | """ 37 | if customGlobals is None: 38 | customGlobals = { 39 | "config": {}, # Will be filled after loading 40 | "module": "", # Will be filled after loading 41 | "os": os, 42 | "socket": socket, 43 | "sys": sys, 44 | } 45 | 46 | logger.info("Loading backend config '%s'", backendConfigFile) 47 | with open(backendConfigFile, encoding="utf-8") as configFile: 48 | exec(configFile.read(), customGlobals) 49 | 50 | config = customGlobals["config"] 51 | logger.debug("Current backend config: %s", config) 52 | 53 | return config 54 | 55 | 56 | def updateConfigFile(backendConfigFile, newConfig, notificationFunction=None): 57 | """ 58 | Updates a config file with the corresponding new configuration. 59 | 60 | :param backendConfigFile: path to the backend configuration 61 | :param newConfig: the new configuration. 62 | :param notificationFunction: A function that log messages will be passed \ 63 | on to. Defaults to logger.notice 64 | :type notificationFunction: func 65 | """ 66 | 67 | def correctBooleans(text): 68 | """ 69 | Creating correct JSON booleans - they are all lowercase. 70 | """ 71 | return text.replace("true", "True").replace("false", "False") 72 | 73 | if notificationFunction is None: 74 | notificationFunction = logger.notice 75 | 76 | notificationFunction(f"Updating backend config '{backendConfigFile}'") 77 | 78 | lines = [] 79 | with codecs.open(backendConfigFile, "r", "utf-8") as backendFile: 80 | for line in backendFile.readlines(): 81 | if re.search(r"^\s*config\s*\=", line): 82 | break 83 | lines.append(line) 84 | 85 | with codecs.open(backendConfigFile, "w", "utf-8") as backendFile: 86 | backendFile.writelines(lines) 87 | backendConfigData = correctBooleans(objectToBeautifiedText(newConfig)) 88 | backendFile.write(f"config = {backendConfigData}\n") 89 | 90 | notificationFunction(f"Backend config '{backendConfigFile}' updated") 91 | 92 | 93 | def _getSysConfig(): 94 | """ 95 | Skinned down version of getSysConfig from ``opsi-setup``. 96 | 97 | Should be used as **fallback only**! 98 | """ 99 | logger.notice("Getting current system config") 100 | fqdn = getLocalFqdn() 101 | sysConfig = {"fqdn": fqdn, "hostname": fqdn.split(".")[0]} 102 | 103 | sysConfig.update(getNetworkConfiguration()) 104 | 105 | logger.notice("System information:") 106 | logger.notice(" ip address : %s", sysConfig["ipAddress"]) 107 | logger.notice(" netmask : %s", sysConfig["netmask"]) 108 | logger.notice(" subnet : %s", sysConfig["subnet"]) 109 | logger.notice(" broadcast : %s", sysConfig["broadcast"]) 110 | logger.notice(" fqdn : %s", sysConfig["fqdn"]) 111 | logger.notice(" hostname : %s", sysConfig["hostname"]) 112 | 113 | return sysConfig 114 | -------------------------------------------------------------------------------- /tests/test_util_windows_drivers.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | Testing WindowsDrivers. 8 | """ 9 | 10 | import os 11 | import pytest 12 | 13 | from OPSI.Util.WindowsDrivers import integrateAdditionalWindowsDrivers 14 | from OPSI.Object import AuditHardwareOnHost 15 | 16 | 17 | def auditHardwareOnHostFactory(hardwareClass, hostId, vendor, model, sku=None): 18 | auditHardwareOnHost = AuditHardwareOnHost(hardwareClass, hostId) 19 | auditHardwareOnHost.vendor = vendor 20 | auditHardwareOnHost.model = model 21 | auditHardwareOnHost.sku = sku 22 | 23 | return auditHardwareOnHost 24 | 25 | 26 | def _generateDirectories(folder, vendor, model): 27 | rulesDir = os.path.join(folder, "byAudit") 28 | if not os.path.exists(rulesDir): 29 | os.mkdir(rulesDir) 30 | vendorDir = os.path.join(rulesDir, vendor) 31 | modelDir = os.path.join(vendorDir, model) 32 | 33 | os.mkdir(vendorDir) 34 | os.mkdir(modelDir) 35 | 36 | 37 | def _generateTestFiles(folder, vendor, model, filename): 38 | dstFilename = os.path.join(folder, "byAudit", vendor, model, filename) 39 | with open(dstFilename, "w"): 40 | pass 41 | 42 | 43 | @pytest.fixture 44 | def destinationDir(tempDir): 45 | yield os.path.join(tempDir, "destination") 46 | 47 | 48 | @pytest.fixture(scope="session") 49 | def hostId(): 50 | yield "test.domain.local" 51 | 52 | 53 | @pytest.fixture(scope="session") 54 | def hardwareClass(): 55 | yield "COMPUTER_SYSTEM" 56 | 57 | 58 | def testByAudit(tempDir, destinationDir, hardwareClass, hostId): 59 | vendor = "Dell Inc." 60 | model = "Venue 11 Pro 7130 MS" 61 | 62 | testData1 = auditHardwareOnHostFactory(hardwareClass, hostId, vendor, model) 63 | _generateDirectories(tempDir, vendor, model) 64 | _generateTestFiles(tempDir, vendor, model, "test.inf") 65 | 66 | result = integrateAdditionalWindowsDrivers( 67 | tempDir, destinationDir, [], auditHardwareOnHosts=[testData1] 68 | ) 69 | 70 | expectedResult = [ 71 | { 72 | "devices": [], 73 | "directory": "%s/1" % destinationDir, 74 | "driverNumber": 1, 75 | "infFile": "%s/1/test.inf" % destinationDir, 76 | } 77 | ] 78 | 79 | assert expectedResult == result 80 | 81 | 82 | def testByAuditWithUnderscoreAtTheEnd(tempDir, destinationDir, hardwareClass, hostId): 83 | vendor = "Dell Inc_" 84 | model = "Venue 11 Pro 7130 MS" 85 | 86 | testData1 = auditHardwareOnHostFactory(hardwareClass, hostId, "Dell Inc.", model) 87 | _generateDirectories(tempDir, vendor, model) 88 | _generateTestFiles(tempDir, vendor, model, "test.inf") 89 | 90 | result = integrateAdditionalWindowsDrivers( 91 | tempDir, destinationDir, [], auditHardwareOnHosts=[testData1] 92 | ) 93 | 94 | expectedResult = [ 95 | { 96 | "devices": [], 97 | "directory": "%s/1" % destinationDir, 98 | "driverNumber": 1, 99 | "infFile": "%s/1/test.inf" % destinationDir, 100 | } 101 | ] 102 | 103 | assert expectedResult == result 104 | 105 | 106 | def testByAuditWithSKUFallback(tempDir, destinationDir, hardwareClass, hostId): 107 | vendor = "Dell Inc_" 108 | model = "Venue 11 Pro 7130 MS (ABC)" 109 | sku = "ABC" 110 | model_without_sku = "Venue 11 Pro 7130 MS" 111 | 112 | testData1 = auditHardwareOnHostFactory( 113 | hardwareClass, hostId, "Dell Inc.", model, sku 114 | ) 115 | _generateDirectories(tempDir, vendor, model_without_sku) 116 | _generateTestFiles(tempDir, vendor, model_without_sku, "test.inf") 117 | 118 | result = integrateAdditionalWindowsDrivers( 119 | tempDir, destinationDir, [], auditHardwareOnHosts=[testData1] 120 | ) 121 | 122 | expectedResult = [ 123 | { 124 | "devices": [], 125 | "directory": "%s/1" % destinationDir, 126 | "driverNumber": 1, 127 | "infFile": "%s/1/test.inf" % destinationDir, 128 | } 129 | ] 130 | 131 | assert expectedResult == result 132 | -------------------------------------------------------------------------------- /data/backendManager/extend.d/20_easy.conf: -------------------------------------------------------------------------------- 1 | def getClients(self): 2 | """ 3 | Returns a list of client hashes. 4 | 5 | These hashes do not include the fields `type` and `id`. 6 | They contain the additional field `depotId` with the assigned depot of the client. 7 | 8 | :rtype: [{}, ] 9 | """ 10 | import re 11 | 12 | timestampRegex = re.compile(r'^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$') 13 | 14 | def convertTimestamp(timestamp): 15 | if timestamp is None: 16 | return '' 17 | 18 | match = timestampRegex.search(client.get('created', '')) 19 | if match: 20 | return '%s%s%s%s%s%s' % (match.group(1), match.group(2), match.group(3), match.group(4), match.group(5), match.group(6)) 21 | 22 | return timestamp 23 | 24 | def _normaliseDict(ddict): 25 | for (key, value) in ddict.items(): 26 | if value is None: 27 | ddict[key] = '' 28 | 29 | return ddict 30 | 31 | clientToDepotservers = { 32 | mapping['clientId']: mapping['depotId'] 33 | for mapping in 34 | self.configState_getClientToDepotserver() 35 | } 36 | 37 | results = [] 38 | for client in self.host_getHashes(type='OpsiClient'): 39 | client['hostId'] = client['id'] 40 | 41 | client['created'] = convertTimestamp(client.get('created')) 42 | client['lastSeen'] = convertTimestamp(client.get('lastSeen')) 43 | 44 | try: 45 | client['depotId'] = clientToDepotservers[client['id']] 46 | except KeyError: 47 | client['depotId'] = '' 48 | 49 | del client['type'] 50 | del client['id'] 51 | 52 | results.append(_normaliseDict(client)) 53 | 54 | return results 55 | 56 | 57 | def getClientIDs(self): 58 | """ 59 | Returns a list of client IDs. 60 | 61 | :rtype: [str, ] 62 | """ 63 | return self.host_getIdents(type="OpsiClient") 64 | 65 | 66 | def getClientsOnDepot(self, depotIds): 67 | """ 68 | Returns a list of client IDs that can be found on the given depots. 69 | 70 | :param depotIds: IDs of depots 71 | :type depotIds: [str, ] 72 | :rtype: list 73 | """ 74 | depotIds = forceHostIdList(depotIds) 75 | if not depotIds: 76 | raise ValueError("No depotIds given") 77 | 78 | return [ 79 | clientToDepotserver['clientId'] 80 | for clientToDepotserver 81 | in self.configState_getClientToDepotserver(depotIds=depotIds) 82 | ] 83 | 84 | 85 | def getClientsWithProducts(self, productIds, installationStatus=None): 86 | """ 87 | Returns a list of client IDs with the given productIds independent from 88 | their status. 89 | This means that this might return clients that had the software in 90 | the past but not currently. 91 | 92 | If `installationStatus` is set only clients with the given status for the 93 | products will be returned. 94 | 95 | :param productIds: The products to search for 96 | :type productIds: [str, ] 97 | :param installationStatus: a specific status to search 98 | :type installationStatus: str 99 | :rtype: [str, ] 100 | """ 101 | productIds = forceProductIdList(productIds) 102 | if not productIds: 103 | raise ValueError("Missing product ids") 104 | 105 | pocFilter = { 106 | "productId": productIds, 107 | } 108 | if installationStatus is not None: 109 | pocFilter['installationStatus'] = forceInstallationStatus(installationStatus) 110 | 111 | clientIds = set([poc.clientId for poc in self.productOnClient_getObjects(**pocFilter)]) 112 | return list(clientIds) 113 | 114 | 115 | def getClientsWithActionRequest(self, actionRequests): 116 | """ 117 | Returns a list of client IDs that have the given actionRequests set. 118 | Each client will only be present once in the list of one of the given action requests match. 119 | 120 | :param actionRequests: The action requests to filter for. 121 | :type actionRequests: str or [str, ] 122 | :rtype: [str, ] 123 | """ 124 | actionRequests = [ 125 | request for request 126 | in forceActionRequestList(actionRequests) 127 | if request 128 | ] 129 | if not actionRequests: 130 | raise ValueError("Missing action requests") 131 | 132 | clientIds = set([poc.clientId for poc in self.productOnClient_getObjects(actionRequest=actionRequests)]) 133 | return list(clientIds) 134 | -------------------------------------------------------------------------------- /tests/data/package_control_file/control: -------------------------------------------------------------------------------- 1 | [Package] 2 | version: 1 3 | depends: 4 | 5 | [Product] 6 | type: localboot 7 | id: dfn_inkscape 8 | name: Inkscape 9 | description: Editor für 2D-Vektorgrafiken im standardisierten SVG-Dateiformat; Import von Bildern und Vektoren, sowie PDF 10 | advice: 11 | version: 0.92.4 12 | priority: 0 13 | licenseRequired: False 14 | productClasses: 15 | setupScript: setup64.opsiscript 16 | uninstallScript: uninstall64.opsiscript 17 | updateScript: 18 | alwaysScript: 19 | onceScript: 20 | customScript: 21 | userLoginScript: 22 | 23 | [ProductDependency] 24 | action: setup 25 | requiredProduct: dfn_ghostscript 26 | requiredStatus: installed 27 | requirementType: before 28 | 29 | [ProductProperty] 30 | type: bool 31 | name: desktop-link 32 | description: Link on Desktop? 33 | default: False 34 | 35 | [ProductProperty] 36 | type: unicode 37 | name: custom-post-install 38 | multivalue: False 39 | editable: False 40 | description: Define filename for include script in custom directory after installation 41 | values: ["none", "post-install.opsiinc"] 42 | default: ["none"] 43 | 44 | [ProductProperty] 45 | type: unicode 46 | name: custom-post-deinstall 47 | multivalue: False 48 | editable: False 49 | description: Define filename for include script in custom directory after deinstallation 50 | values: ["none", "post-deinstall.opsiinc"] 51 | default: ["none"] 52 | 53 | [ProductProperty] 54 | type: unicode 55 | name: silent-option 56 | multivalue: False 57 | editable: False 58 | description: Un/Install MSI silent (/qb!) or very silent (/qn) 59 | values: ["/qb!", "/qn"] 60 | default: ["/qb!"] 61 | 62 | [Changelog] 63 | dfn_inkscape (0.92.4-1) 64 | * neue Upstreamversion (http://wiki.inkscape.org/wiki/index.php/Release_notes/0.92.4) 65 | -- Thomas Besser (archIT/KIT) , 21.01.2019 66 | 67 | dfn_inkscape (0.92.3-2) 68 | * neues o4i-Logo 69 | * neue Registrysuche (https://github.com/opsi4instituts/lib, winst-Version 4.12.0.16 Voraussetzung) 70 | * Verwendung uib_exitcode (local function) 71 | * Check Version (Paket <-> Installation) 72 | -- David Dams (archIT/KIT) , 07.01.2019 73 | 74 | dfn_inkscape (0.92.3-1) 75 | * neue Upstreamversion (http://wiki.inkscape.org/wiki/index.php/Release_notes/0.92.3) 76 | * o4i-Kosmetik (desktoplink -> desktop-link, msi-silent-option -> silent-option) 77 | -- Thomas Besser (archIT/KIT) , 26.03.2018 78 | 79 | dfn_inkscape (0.92.2-1) 80 | * neue Upstreamversion (stability and bugfix release) 81 | * alte uib Copyrights (Überbleibsel von opsi-template) entfernt 82 | * Desktopicon -> Desktoplink gem. o4i-Richtlinie angepasst 83 | * o4i-Logo: Anzeigeaufruf nach common.opsiinc ausgelagert, eigenes Logo möglich 84 | -- Thomas Besser (archIT/KIT) , 17.02.2017 85 | 86 | dfn_inkscape (0.92.1-1) 87 | * neue Upstreamversion (stability and bugfix release) 88 | * Minor-Versionsnummer via $InstFile$ 89 | -- Thomas Besser (archIT/KIT) , 17.02.2017 90 | 91 | dfn_inkscape (0.92-1) 92 | * o4i-Logo, MSI-Check-Exitcode, ProductProperty MSISilentOption hinzugefügt 93 | * Check auf 64-Bit-System bzw. Win-Version nach common.opsiinc 94 | * ProductProperty install_architecture entfernt, da nur 64-Bit im Paket 95 | * Version aus Paket holen für $InstFile$ 96 | -- Thomas Besser (archIT/KIT) , 09.01.2017 97 | 98 | dfn_inkscape (0.91-2) 99 | * Copy&Paste-Überbleibsel entfernt ;-) 100 | * Bugfix "InstallLocation" bzw. "DisplayIcon" an die richtige Stelle in Registry schreiben 101 | * Icon hinzugefügt 102 | -- Thomas Besser (archIT/KIT) , 13.08.2015 103 | 104 | dfn_inkscape (0.91-1) 105 | * initiales DFN-Paket 106 | * angepasstes MSI-Paket, das kein Desktopicon anlegt 107 | * MSI speichert 'InstallLocation' nicht ab bzw. 'DisplayIcon' fehlt -> manuell in Registry schreiben 108 | -- Thomas Besser (archIT/KIT) , 12.08.2015 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /data/backendManager/extend.d/45_deprecated.conf: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # DEPRECATED METHODS 5 | # ------------------ 6 | # 7 | # This module contains methods that are marked as deprecated. 8 | # They will be removed with the next major release or minor release. 9 | # 10 | # If you are making use of these methods you should replace their usage. 11 | # If there is an easy way to replace the calls a call to a deprecated 12 | # method will issue an warning with information on what method can be 13 | # used instead. 14 | # 15 | 16 | @deprecated(alternative_method='backend_createBase') 17 | def createOpsiBase(self): 18 | self.backend_createBase() 19 | 20 | 21 | @deprecated(alternative_method='host_createOpsiConfigserver') 22 | def createServer(self, serverName, domain, description=None, notes=None): 23 | id = forceHostId('.'.join((forceHostname(serverName), forceDomain(domain)))) 24 | self.host_createOpsiConfigserver(id=id, description=description, notes=notes) 25 | return id 26 | 27 | 28 | @deprecated(alternative_method='host_delete') 29 | def deleteClient(self, clientId): 30 | self.host_delete(id=forceHostId(clientId)) 31 | 32 | 33 | @deprecated(alternative_method='host_delete') 34 | def deleteDepot(self, depotId): 35 | self.host_delete(id=forceHostId(depotId)) 36 | 37 | 38 | @deprecated(alternative_method='group_delete') 39 | def deleteGroup(self, groupId): 40 | self.group_delete(id=groupId) 41 | 42 | 43 | @deprecated 44 | def deleteProductDependency(self, productId, action="", requiredProductId="", requiredProductClassId="", requirementType="", depotIds=[]): 45 | if not action: 46 | action = None 47 | if not requiredProductId: 48 | requiredProductId = None 49 | if not depotIds: 50 | depotIds = [] 51 | 52 | # Warn users relying on obsolete attributes 53 | if requiredProductClassId: 54 | logger.warning("The argument 'requiredProductClassId' is obsolete and has no effect.") 55 | if requirementType: 56 | logger.warning("The argument 'requirementType' is obsolete and has no effect.") 57 | 58 | for productOnDepot in self.productOnDepot_getObjects(productId=productId, depotId=depotIds): 59 | self.productDependency_delete( 60 | productId=productOnDepot.productId, 61 | productVersion=productOnDepot.productVersion, 62 | packageVersion=productOnDepot.packageVersion, 63 | productAction=action, 64 | requiredProductId=requiredProductId 65 | ) 66 | 67 | 68 | @deprecated(alternative_method='host_delete') 69 | def deleteServer(self, serverId): 70 | self.host_delete(id=forceHostId(serverId)) 71 | 72 | 73 | @deprecated 74 | def setHostLastSeen(self, hostId, timestamp): 75 | hostId = forceHostId(hostId) 76 | hosts = self.host_getObjects(id=hostId) 77 | if not hosts: 78 | raise BackendMissingDataError("Host '%s' not found" % hostId) 79 | hosts[0].setLastSeen(timestamp) 80 | self.host_updateObject(hosts[0]) 81 | 82 | 83 | @deprecated(alternative_method='getClients') 84 | def getClients_listOfHashes(self, serverId=None, depotIds=[], groupId=None, productId=None, installationStatus=None, actionRequest=None, productVersion=None, packageVersion=None, hwFilter=None): 85 | if serverId or depotIds or groupId or productId or installationStatus or actionRequest or productVersion or packageVersion or hwFilter: 86 | raise RuntimeError("These parameters have been deprecated") 87 | 88 | return self.getClients() 89 | 90 | 91 | @deprecated(alternative_method='getClientIDs') 92 | def getClientIds_list(self, serverId=None, depotIds=[], groupId=None, productId=None, installationStatus=None, actionRequest=None, productVersion=None, packageVersion=None, hwFilter=None): 93 | if not (serverId or depotIds or groupId or productId or installationStatus or actionRequest or productVersion or packageVersion or hwFilter): 94 | return self.getClientIDs() 95 | if depotIds: 96 | return self.getClientsOnDepot(depotIds) 97 | if productId and installationStatus: 98 | return self.getClientsWithProducts(productId, installationStatus=installationStatus) 99 | if productId: 100 | return self.getClientsWithProducts(productId) 101 | if actionRequest: 102 | return self.getClientsWithActionRequest(actionRequest) 103 | 104 | raise RuntimeError("Missing parameters for mapping getClientIds_list to replacing method.") 105 | -------------------------------------------------------------------------------- /OPSI/Backend/HostControlSafe.py: -------------------------------------------------------------------------------- 1 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 2 | # Copyright (c) 2008-2025 uib GmbH 3 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 4 | # License: AGPL-3.0-only 5 | 6 | """ 7 | HostControl Backend: Safe edition 8 | """ 9 | 10 | from typing import Any, List 11 | 12 | from OPSI.Backend.Base.Backend import Backend 13 | from OPSI.Backend.HostControl import HostControlBackend 14 | from OPSI.Exceptions import BackendMissingDataError 15 | 16 | __all__ = ("HostControlSafeBackend",) 17 | 18 | 19 | class HostControlSafeBackend(HostControlBackend): 20 | """ 21 | This backend is the same as the HostControl-backend but it will not 22 | allow to call methods without hostId 23 | """ 24 | 25 | def __init__(self, backend: Backend, **kwargs) -> None: 26 | self._name = "hostcontrolsafe" 27 | HostControlBackend.__init__(self, backend, **kwargs) 28 | 29 | def hostControlSafe_start(self, hostIds: list[str] = None) -> dict[str, Any]: 30 | """Switches on remote computers using WOL.""" 31 | if not hostIds: 32 | raise BackendMissingDataError("No matching host ids found") 33 | return HostControlBackend.hostControl_start(self, hostIds or []) 34 | 35 | def hostControlSafe_shutdown(self, hostIds: list[str] = None) -> dict[str, Any]: 36 | if not hostIds: 37 | raise BackendMissingDataError("No matching host ids found") 38 | return HostControlBackend.hostControl_shutdown(self, hostIds or []) 39 | 40 | def hostControlSafe_reboot(self, hostIds: list[str] = None) -> dict[str, Any]: 41 | if not hostIds: 42 | raise BackendMissingDataError("No matching host ids found") 43 | return HostControlBackend.hostControl_reboot(self, hostIds or []) 44 | 45 | def hostControlSafe_fireEvent( 46 | self, event: str, hostIds: list[str] = None 47 | ) -> dict[str, Any]: 48 | if not hostIds: 49 | raise BackendMissingDataError("No matching host ids found") 50 | return HostControlBackend.hostControl_fireEvent(self, event, hostIds or []) 51 | 52 | def hostControlSafe_showPopup( 53 | self, 54 | message: str, 55 | hostIds: list[str] = None, 56 | mode: str = "prepend", 57 | addTimestamp: bool = True, 58 | displaySeconds: float = 0, 59 | ) -> dict[str, Any]: 60 | if not hostIds: 61 | raise BackendMissingDataError("No matching host ids found") 62 | return HostControlBackend.hostControl_showPopup( 63 | self, message, hostIds or [], mode, addTimestamp, displaySeconds 64 | ) 65 | 66 | def hostControlSafe_uptime(self, hostIds: list[str] = None) -> dict[str, Any]: 67 | if not hostIds: 68 | raise BackendMissingDataError("No matching host ids found") 69 | return HostControlBackend.hostControl_uptime(self, hostIds or []) 70 | 71 | def hostControlSafe_getActiveSessions( 72 | self, hostIds: list[str] = None 73 | ) -> dict[str, Any]: 74 | if not hostIds: 75 | raise BackendMissingDataError("No matching host ids found") 76 | return HostControlBackend.hostControl_getActiveSessions(self, hostIds or []) 77 | 78 | def hostControlSafe_opsiclientdRpc( 79 | self, 80 | method: str, 81 | params: List = None, 82 | hostIds: list[str] = None, 83 | timeout: int = None, 84 | ) -> dict[str, Any]: 85 | if not hostIds: 86 | raise BackendMissingDataError("No matching host ids found") 87 | return HostControlBackend.hostControl_opsiclientdRpc( 88 | self, method, params or [], hostIds or [], timeout 89 | ) 90 | 91 | def hostControlSafe_reachable( 92 | self, hostIds: list[str] = None, timeout: int = None 93 | ) -> dict[str, Any]: 94 | if not hostIds: 95 | raise BackendMissingDataError("No matching host ids found") 96 | return HostControlBackend.hostControl_reachable(self, hostIds or [], timeout) 97 | 98 | def hostControlSafe_execute( 99 | self, 100 | command: str, 101 | hostIds: list[str] = None, 102 | waitForEnding: bool = True, 103 | captureStderr: bool = True, 104 | encoding: str = None, 105 | timeout: int = 300, 106 | ): 107 | if not hostIds: 108 | raise BackendMissingDataError("No matching host ids found") 109 | return HostControlBackend.hostControl_execute( 110 | self, 111 | command, 112 | hostIds or [], 113 | waitForEnding, 114 | captureStderr, 115 | encoding, 116 | timeout, 117 | ) 118 | -------------------------------------------------------------------------------- /tests/test_backend_extend_d_70_wan.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # python-opsi is part of the desktop management solution opsi http://www.opsi.org 3 | # Copyright (c) 2008-2025 uib GmbH 4 | # This code is owned by the uib GmbH, Mainz, Germany (uib.de). All rights reserved. 5 | # License: AGPL-3.0-only 6 | 7 | """ 8 | Tests for easy configuration of WAN clients. 9 | 10 | This tests what usually is found under 11 | ``/etc/opsi/backendManager/extend.de/70_wan.conf``. 12 | 13 | .. versionadded:: 4.0.6.3 14 | """ 15 | 16 | from __future__ import print_function 17 | 18 | import pytest 19 | 20 | from OPSI.Object import OpsiClient 21 | from OPSI.Util.Task.ConfigureBackend.ConfigurationData import createWANconfigs 22 | 23 | 24 | @pytest.fixture 25 | def backendWithWANConfigs(backendManager): 26 | createWANconfigs(backendManager) 27 | yield backendManager 28 | 29 | 30 | def clientHasWANEnabled(backend, clientId): 31 | configsToCheck = set( 32 | [ 33 | "opsiclientd.event_gui_startup.active", 34 | "opsiclientd.event_gui_startup{user_logged_in}.active", 35 | "opsiclientd.event_net_connection.active", 36 | "opsiclientd.event_timer.active", 37 | ] 38 | ) 39 | 40 | for configState in backend.configState_getObjects(objectId=clientId): 41 | if configState.configId == "opsiclientd.event_gui_startup.active": 42 | if configState.values[0]: 43 | return False 44 | configsToCheck.remove("opsiclientd.event_gui_startup.active") 45 | elif ( 46 | configState.configId 47 | == "opsiclientd.event_gui_startup{user_logged_in}.active" 48 | ): 49 | if configState.values[0]: 50 | return False 51 | configsToCheck.remove( 52 | "opsiclientd.event_gui_startup{user_logged_in}.active" 53 | ) 54 | elif configState.configId == "opsiclientd.event_net_connection.active": 55 | if not configState.values[0]: 56 | return False 57 | configsToCheck.remove("opsiclientd.event_net_connection.active") 58 | elif configState.configId == "opsiclientd.event_timer.active": 59 | if not configState.values[0]: 60 | return False 61 | configsToCheck.remove("opsiclientd.event_timer.active") 62 | 63 | if configsToCheck: 64 | print("The following configs were not set: {0}".format(configsToCheck)) 65 | return False 66 | 67 | return True 68 | 69 | 70 | def testEnablingSettingForOneHost(backendWithWANConfigs): 71 | backend = backendWithWANConfigs 72 | clientId = "testclient.test.invalid" 73 | backend.host_createObjects(OpsiClient(id=clientId)) 74 | 75 | backend.changeWANConfig(True, clientId) 76 | assert clientHasWANEnabled(backend, clientId) 77 | 78 | backend.changeWANConfig(False, clientId) 79 | assert not clientHasWANEnabled(backend, clientId) 80 | 81 | 82 | def testEnablingSettingForMultipleHosts(backendWithWANConfigs): 83 | backend = backendWithWANConfigs 84 | 85 | clientIds = ["testclient{0}.test.invalid".format(num) for num in range(10)] 86 | backend.host_createObjects([OpsiClient(id=clientId) for clientId in clientIds]) 87 | 88 | backend.changeWANConfig(True, clientIds) 89 | 90 | for clientId in clientIds: 91 | assert clientHasWANEnabled(backend, clientId) 92 | 93 | 94 | def testNotFailingOnEmptyList(backendWithWANConfigs): 95 | backendWithWANConfigs.changeWANConfig(True, []) 96 | 97 | 98 | def testNotChangingUnreferencedClient(backendWithWANConfigs): 99 | backend = backendWithWANConfigs 100 | 101 | clientIds = ["testclient{0}.test.invalid".format(num) for num in range(10)] 102 | singleClient = "testclient99.test.invalid" 103 | backend.host_createObjects([OpsiClient(id=clientId) for clientId in clientIds]) 104 | backend.host_createObjects([OpsiClient(id=singleClient)]) 105 | 106 | backend.changeWANConfig(True, clientIds) 107 | backend.changeWANConfig(True, []) 108 | 109 | for clientId in clientIds: 110 | assert clientHasWANEnabled(backend, clientId) 111 | 112 | assert not clientHasWANEnabled(backend, singleClient) 113 | 114 | 115 | @pytest.mark.parametrize( 116 | "value, expected", 117 | [ 118 | ("on", True), 119 | ("1", True), 120 | ("true", True), 121 | ("off", False), 122 | ("false", False), 123 | ("0", False), 124 | ], 125 | ) 126 | def testUsingNonBooleanParameters(backendWithWANConfigs, value, expected): 127 | backend = backendWithWANConfigs 128 | 129 | client = OpsiClient(id="testclient101.test.invalid") 130 | backend.host_createObjects([client]) 131 | 132 | backend.changeWANConfig(value, client.id) 133 | assert clientHasWANEnabled(backend, client.id) == expected 134 | --------------------------------------------------------------------------------