├── .gitignore ├── .gitlab-ci.yml ├── .vscode ├── extensions.json └── settings.json ├── OPSI ├── Backend │ ├── Backend.py │ ├── BackendManager.py │ ├── Base │ │ ├── Backend.py │ │ ├── ConfigData.py │ │ ├── Extended.py │ │ ├── ModificationTracking.py │ │ └── __init__.py │ ├── DHCPD.py │ ├── Depotserver.py │ ├── File.py │ ├── HostControl.py │ ├── HostControlSafe.py │ ├── JSONRPC.py │ ├── Manager │ │ ├── AccessControl.py │ │ ├── Authentication │ │ │ ├── LDAP.py │ │ │ ├── NT.py │ │ │ ├── PAM.py │ │ │ └── __init__.py │ │ ├── Config.py │ │ ├── Dispatcher.py │ │ ├── Extender.py │ │ ├── _Manager.py │ │ └── __init__.py │ ├── MySQL.py │ ├── OpsiPXEConfd.py │ ├── Replicator.py │ ├── SQL.py │ ├── SQLite.py │ └── __init__.py ├── Config.py ├── Exceptions.py ├── Logger.py ├── Object.py ├── System │ ├── Darwin.py │ ├── Linux.py │ ├── Posix.py │ ├── Windows.py │ └── __init__.py ├── Types.py ├── UI.py ├── Util │ ├── File │ │ ├── Archive │ │ │ └── __init__.py │ │ ├── Opsi │ │ │ ├── Opsirc.py │ │ │ └── __init__.py │ │ └── __init__.py │ ├── Log.py │ ├── Message.py │ ├── Path.py │ ├── Ping.py │ ├── Product.py │ ├── Repository.py │ ├── Sync.py │ ├── Task │ │ ├── BackendMigration.py │ │ ├── Backup.py │ │ ├── CleanupBackend.py │ │ ├── ConfigureBackend │ │ │ ├── ConfigDefaults.py │ │ │ ├── ConfigurationData.py │ │ │ ├── DHCPD.py │ │ │ ├── MySQL.py │ │ │ └── __init__.py │ │ ├── InitializeBackend.py │ │ ├── UpdateBackend │ │ │ ├── ConfigurationData.py │ │ │ ├── File.py │ │ │ ├── MySQL.py │ │ │ └── __init__.py │ │ └── __init__.py │ ├── Thread.py │ ├── WIM.py │ ├── WindowsDrivers.py │ └── __init__.py └── __init__.py ├── README.md ├── gettext ├── python-opsi.pot ├── python-opsi_da.po ├── python-opsi_de.po ├── python-opsi_en.po ├── python-opsi_es.po ├── python-opsi_fr.po ├── python-opsi_it.po ├── python-opsi_nl.po ├── python-opsi_pl.po └── python-opsi_ru.po ├── opsi-dev-tool.yml ├── poetry.lock ├── poetry.toml ├── pyproject.toml └── tests ├── Backends ├── File.py ├── MySQL.py ├── SQLite.py ├── __init__.py ├── config.py.example └── config.py.gitlabci ├── __init__.py ├── conftest.py ├── data ├── backend │ ├── dhcp_ki.conf │ ├── small_extended_hwaudit.conf │ ├── small_hwaudit.conf │ └── testingproduct_23-42.opsi ├── package_control_file │ ├── changelog.txt │ ├── control │ └── control.yml ├── system │ └── posix │ │ └── dhclient.leases ├── util │ ├── davxml │ │ └── twisted-davxml.data │ ├── dhcpd │ │ ├── dhcpd_1.conf │ │ └── link_to_dhcpd1_1.conf │ ├── fake_global.conf │ ├── file │ │ ├── inf_testdata_1.inf │ │ ├── inf_testdata_2.inf │ │ ├── inf_testdata_3.inf │ │ ├── inf_testdata_4.inf │ │ ├── inf_testdata_5.inf │ │ ├── inf_testdata_6.inf │ │ ├── inf_testdata_7.inf │ │ ├── inf_testdata_8.inf │ │ ├── opsi-configed_4.0.7.1.3-2.opsi.zsync │ │ ├── opsi │ │ │ ├── control.toml │ │ │ ├── control_with_empty_property_values │ │ │ ├── control_with_german_umlauts │ │ │ ├── control_with_special_characters_in_property │ │ │ ├── control_without_versions │ │ │ └── opsi.conf │ │ ├── txtsetupoem_testdata_1.oem │ │ ├── txtsetupoem_testdata_2.oem │ │ ├── txtsetupoem_testdata_3.oem │ │ ├── txtsetupoem_testdata_4.oem │ │ ├── txtsetupoem_testdata_5.oem │ │ ├── txtsetupoem_testdata_6.oem │ │ └── txtsetupoem_testdata_7.oem │ ├── syncFiles │ │ └── librsyncSignature.txt │ └── task │ │ ├── certificate │ │ ├── corrupt.pem │ │ ├── example.pem │ │ └── invalid.pem │ │ ├── smb.conf │ │ ├── sudoers │ │ └── sudoers_without_entries │ │ └── updatePackages │ │ ├── apachelisting.html │ │ ├── example_updater.conf │ │ └── experimental.repo └── wimlib.example ├── helpers.py ├── test_acl.py ├── test_backend_backend.py ├── test_backend_backenddispatcher.py ├── test_backend_backendmanager.py ├── test_backend_configdatabackend.py ├── test_backend_extend_d_10_opsi.py ├── test_backend_extend_d_10_wim.py ├── test_backend_extend_d_20_easy.py ├── test_backend_extend_d_20_legacy.py ├── test_backend_extend_d_30_kiosk.py ├── test_backend_extend_d_30_sshcommands.py ├── test_backend_extend_d_40_admin_tasks.py ├── test_backend_extend_d_40_group_actions.py ├── test_backend_extend_d_70_dynamic_depot.py ├── test_backend_extend_d_70_wan.py ├── test_backend_extendedconfigdatabackend.py ├── test_backend_file.py ├── test_backend_hostcontrol.py ├── test_backend_jsonrpc.py ├── test_backend_methods.py ├── test_backend_modificationtracker.py ├── test_backend_multithreading.py ├── test_backend_opsipxeconfd.py ├── test_backend_replicator.py ├── test_backend_sql.py ├── test_backend_sqlite.py ├── test_config.py ├── test_configs.py ├── test_groups.py ├── test_hardware_audit.py ├── test_hosts.py ├── test_license_management.py ├── test_logger.py ├── test_products.py ├── test_software_and_hardware_audit.py ├── test_system.py ├── test_system_posix.py ├── test_system_posix_distribution.py ├── test_util.py ├── test_util_file.py ├── test_util_file_archive.py ├── test_util_file_dhcpdconf.py ├── test_util_file_opsi.py ├── test_util_file_opsi_backup.py ├── test_util_file_opsi_opsirc.py ├── test_util_product.py ├── test_util_repository.py ├── test_util_sync.py ├── test_util_task_backup.py ├── test_util_task_cleanup_backend.py ├── test_util_task_configure_backend.py ├── test_util_task_configure_backend_dhcp.py ├── test_util_task_initialize_backend.py ├── test_util_task_update_backend_data.py ├── test_util_task_update_backend_file.py ├── test_util_task_update_backend_mysql.py ├── test_util_thread.py ├── test_util_wim.py └── test_util_windows_drivers.py /.gitignore: -------------------------------------------------------------------------------- 1 | /.venv 2 | /build 3 | /dist 4 | /data 5 | /python-opsi_*.dsc 6 | /python-opsi_*.tar.gz 7 | /python-opsi_*.deb 8 | pip-wheel-metadata 9 | *~ 10 | *.pyc 11 | *.pyo 12 | .settings 13 | .project 14 | .pydevproject 15 | *.egg-info 16 | *.sublime-* 17 | *.idea/ 18 | tests/Backends/config.py 19 | *.mo 20 | 21 | # QA files: 22 | .coverage 23 | coverage.xml 24 | nosetests.xml 25 | testreport-OPSI.xml 26 | 27 | build/ 28 | data/version 29 | debian/files 30 | debian/python-opsi.debhelper.log 31 | debian/python-opsi.postinst.debhelper 32 | debian/python-opsi.prerm.debhelper 33 | debian/python-opsi.substvars 34 | debian/python-opsi/ 35 | locale/ 36 | 37 | 38 | -------------------------------------------------------------------------------- /.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 | stage: test 19 | script: | 20 | mkdir -p /etc/opsi/licenses 21 | mkdir -p /var/log/opsi 22 | 23 | # Installing opsi test license 24 | [ -z "${OPSILICSRV_TOKEN}" ] && (echo "OPSILICSRV_TOKEN not set" 1>&2 ; exit 1) 25 | 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 26 | 27 | # cloning opsi-server for data directory 28 | rm -rf ../opsi-server 29 | rm -rf data 30 | git clone -b v4.2 https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.uib.gmbh/uib/opsi-server.git ../opsi-server 31 | ln -s ../opsi-server/opsi-server_data/etc data 32 | cp tests/Backends/config.py.gitlabci tests/Backends/config.py 33 | apt-get update 34 | apt-get --yes install default-libmysqlclient-dev librsync1 35 | poetry env use 3.13 36 | poetry lock 37 | poetry install 38 | poetry run ruff check OPSI tests 39 | poetry add mysqlclient==2.1.1 40 | poetry run pytest --tb=short -x -o junit_family=xunit2 --junitxml=testreport.xml --cov-append --cov-report term --cov-report xml -v tests 41 | coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+)%/' 42 | artifacts: 43 | name: 'python-opsi_test' 44 | paths: 45 | - coverage.xml 46 | - testreport.xml 47 | reports: 48 | junit: testreport.xml 49 | expire_in: 14 days 50 | 51 | 52 | apidoc: 53 | stage: doc 54 | when: manual 55 | before_script: 56 | - 'which ssh-agent || (apt update && apt -y install openssh-client)' 57 | - 'which rsync || (apt update && apt -y install rsync)' 58 | - mkdir -p ~/.ssh 59 | - eval $(ssh-agent -s) 60 | - ssh-add <(echo "$BLOG_PUBLISH_PRIVATE_KEY") 61 | script: 62 | - poetry env use 3.13 63 | - poetry lock 64 | - poetry install 65 | - poetry run opsi-dev-cli apidoc makehtml --output python-opsi 66 | - ssh -o StrictHostKeyChecking=no "root@docker1.ext.uib.gmbh" "mkdir -p /var/lib/docker/volumes/docs_nginx_data/_data/python-docs" 67 | - 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/" 68 | 69 | 70 | uibpypi: 71 | stage: publish 72 | script: 73 | - poetry env use 3.13 74 | - poetry lock 75 | - poetry install 76 | - poetry run opsi-dev-tool -l info --uib-pypi-publish 77 | only: 78 | - tags 79 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /OPSI/Backend/Backend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Basic backend. 7 | 8 | This holds the basic backend classes. 9 | """ 10 | 11 | import threading 12 | from contextlib import contextmanager 13 | from typing import Any, Callable 14 | 15 | from .Base import ( 16 | Backend, 17 | BackendModificationListener, 18 | ConfigDataBackend, 19 | ExtendedBackend, 20 | ExtendedConfigDataBackend, 21 | ModificationTrackingBackend, 22 | describeInterface, 23 | ) 24 | 25 | __all__ = ( 26 | "describeInterface", 27 | "temporaryBackendOptions", 28 | "DeferredCall", 29 | "Backend", 30 | "ExtendedBackend", 31 | "ConfigDataBackend", 32 | "ExtendedConfigDataBackend", 33 | "ModificationTrackingBackend", 34 | "BackendModificationListener", 35 | ) 36 | 37 | 38 | @contextmanager 39 | def temporaryBackendOptions(backend: Backend, **options) -> None: 40 | oldOptions = backend.backend_getOptions() 41 | try: 42 | backend.backend_setOptions(options) 43 | yield 44 | finally: 45 | backend.backend_setOptions(oldOptions) 46 | 47 | 48 | class DeferredCall: 49 | def __init__(self, callback: Callable = None) -> None: 50 | self.error = None 51 | self.result = None 52 | self.finished = threading.Event() 53 | self.callback = callback 54 | self.callbackArgs = [] 55 | self.callbackKwargs = {} 56 | 57 | def waitForResult(self) -> Any: 58 | self.finished.wait() 59 | if self.error: 60 | raise self.error 61 | return self.result 62 | 63 | def setCallback(self, callback: Callable, *args, **kwargs) -> None: 64 | self.callback = callback 65 | self.callbackArgs = args 66 | self.callbackKwargs = kwargs 67 | 68 | def _gotResult(self) -> None: 69 | self.finished.set() 70 | if self.callback: 71 | self.callback(self, *self.callbackArgs, **self.callbackKwargs) 72 | -------------------------------------------------------------------------------- /OPSI/Backend/BackendManager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | BackendManager. 7 | 8 | If you want to work with an opsi backend in i.e. a script a 9 | BackendManager instance should be your first choice. 10 | A BackendManager instance does the heavy lifting for you so you don't 11 | need to set up you backends, ACL, multiplexing etc. yourself. 12 | """ 13 | 14 | from .Manager._Manager import BackendManager, backendManagerFactory 15 | from .Manager.AccessControl import BackendAccessControl 16 | from .Manager.Dispatcher import BackendDispatcher 17 | from .Manager.Extender import BackendExtender 18 | 19 | __all__ = ( 20 | "BackendManager", 21 | "BackendDispatcher", 22 | "BackendExtender", 23 | "BackendAccessControl", 24 | "backendManagerFactory", 25 | ) 26 | -------------------------------------------------------------------------------- /OPSI/Backend/Base/ModificationTracking.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Backend that tracks modifications. 7 | """ 8 | 9 | from opsicommon.logging import get_logger 10 | 11 | from .Extended import ExtendedBackend 12 | 13 | __all__ = ("ModificationTrackingBackend", "BackendModificationListener") 14 | 15 | 16 | logger = get_logger("opsi.general") 17 | 18 | 19 | class ModificationTrackingBackend(ExtendedBackend): 20 | def __init__(self, backend, overwrite=True): 21 | ExtendedBackend.__init__(self, backend, overwrite=overwrite) 22 | self._createInstanceMethods() 23 | self._backendChangeListeners = [] 24 | 25 | def addBackendChangeListener(self, backendChangeListener): 26 | if backendChangeListener in self._backendChangeListeners: 27 | return 28 | self._backendChangeListeners.append(backendChangeListener) 29 | 30 | def removeBackendChangeListener(self, backendChangeListener): 31 | if backendChangeListener not in self._backendChangeListeners: 32 | return 33 | self._backendChangeListeners.remove(backendChangeListener) 34 | 35 | def _fireEvent(self, event, *args): 36 | for bcl in self._backendChangeListeners: 37 | try: 38 | meth = getattr(bcl, event) 39 | meth(self, *args) 40 | except Exception as err: 41 | logger.error(err) 42 | 43 | def _executeMethod(self, methodName, **kwargs): 44 | logger.debug( 45 | "ModificationTrackingBackend %s: executing %s on backend %s", 46 | self, 47 | methodName, 48 | self._backend, 49 | ) 50 | meth = getattr(self._backend, methodName) 51 | result = meth(**kwargs) 52 | action = None 53 | if "_" in methodName: 54 | action = methodName.split("_", 1)[1] 55 | 56 | if action in ("insertObject", "updateObject", "deleteObjects"): 57 | value = list(kwargs.values())[0] 58 | if action == "insertObject": 59 | self._fireEvent("objectInserted", value) 60 | elif action == "updateObject": 61 | self._fireEvent("objectUpdated", value) 62 | elif action == "deleteObjects": 63 | self._fireEvent("objectsDeleted", value) 64 | self._fireEvent("backendModified") 65 | 66 | return result 67 | 68 | 69 | class BackendModificationListener: 70 | def objectInserted(self, backend, obj): 71 | # Should return immediately! 72 | pass 73 | 74 | def objectUpdated(self, backend, obj): 75 | # Should return immediately! 76 | pass 77 | 78 | def objectsDeleted(self, backend, objs): 79 | # Should return immediately! 80 | pass 81 | 82 | def backendModified(self, backend): 83 | # Should return immediately! 84 | pass 85 | -------------------------------------------------------------------------------- /OPSI/Backend/Base/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Backends. 7 | """ 8 | 9 | from __future__ import absolute_import 10 | 11 | from .Backend import Backend, describeInterface 12 | from .ConfigData import ConfigDataBackend 13 | from .Extended import ExtendedBackend, ExtendedConfigDataBackend 14 | from .ModificationTracking import ( 15 | BackendModificationListener, 16 | ModificationTrackingBackend, 17 | ) 18 | 19 | __all__ = ( 20 | "describeInterface", 21 | "Backend", 22 | "ExtendedBackend", 23 | "ConfigDataBackend", 24 | "ExtendedConfigDataBackend", 25 | "ModificationTrackingBackend", 26 | "BackendModificationListener", 27 | ) 28 | -------------------------------------------------------------------------------- /OPSI/Backend/HostControlSafe.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | HostControl Backend: Safe edition 7 | """ 8 | 9 | from typing import Any, List 10 | 11 | from OPSI.Backend.Base.Backend import Backend 12 | from OPSI.Backend.HostControl import HostControlBackend 13 | from OPSI.Exceptions import BackendMissingDataError 14 | 15 | __all__ = ("HostControlSafeBackend",) 16 | 17 | 18 | class HostControlSafeBackend(HostControlBackend): 19 | """ 20 | This backend is the same as the HostControl-backend but it will not 21 | allow to call methods without hostId 22 | """ 23 | 24 | def __init__(self, backend: Backend, **kwargs) -> None: 25 | self._name = "hostcontrolsafe" 26 | HostControlBackend.__init__(self, backend, **kwargs) 27 | 28 | def hostControlSafe_start(self, hostIds: list[str] = None) -> dict[str, Any]: 29 | """Switches on remote computers using WOL.""" 30 | if not hostIds: 31 | raise BackendMissingDataError("No matching host ids found") 32 | return HostControlBackend.hostControl_start(self, hostIds or []) 33 | 34 | def hostControlSafe_shutdown(self, hostIds: list[str] = None) -> dict[str, Any]: 35 | if not hostIds: 36 | raise BackendMissingDataError("No matching host ids found") 37 | return HostControlBackend.hostControl_shutdown(self, hostIds or []) 38 | 39 | def hostControlSafe_reboot(self, hostIds: list[str] = None) -> dict[str, Any]: 40 | if not hostIds: 41 | raise BackendMissingDataError("No matching host ids found") 42 | return HostControlBackend.hostControl_reboot(self, hostIds or []) 43 | 44 | def hostControlSafe_fireEvent( 45 | self, event: str, hostIds: list[str] = None 46 | ) -> dict[str, Any]: 47 | if not hostIds: 48 | raise BackendMissingDataError("No matching host ids found") 49 | return HostControlBackend.hostControl_fireEvent(self, event, hostIds or []) 50 | 51 | def hostControlSafe_showPopup( 52 | self, 53 | message: str, 54 | hostIds: list[str] = None, 55 | mode: str = "prepend", 56 | addTimestamp: bool = True, 57 | displaySeconds: float = 0, 58 | ) -> dict[str, Any]: 59 | if not hostIds: 60 | raise BackendMissingDataError("No matching host ids found") 61 | return HostControlBackend.hostControl_showPopup( 62 | self, message, hostIds or [], mode, addTimestamp, displaySeconds 63 | ) 64 | 65 | def hostControlSafe_uptime(self, hostIds: list[str] = None) -> dict[str, Any]: 66 | if not hostIds: 67 | raise BackendMissingDataError("No matching host ids found") 68 | return HostControlBackend.hostControl_uptime(self, hostIds or []) 69 | 70 | def hostControlSafe_getActiveSessions( 71 | self, hostIds: list[str] = None 72 | ) -> dict[str, Any]: 73 | if not hostIds: 74 | raise BackendMissingDataError("No matching host ids found") 75 | return HostControlBackend.hostControl_getActiveSessions(self, hostIds or []) 76 | 77 | def hostControlSafe_opsiclientdRpc( 78 | self, 79 | method: str, 80 | params: List = None, 81 | hostIds: list[str] = None, 82 | timeout: int = None, 83 | ) -> dict[str, Any]: 84 | if not hostIds: 85 | raise BackendMissingDataError("No matching host ids found") 86 | return HostControlBackend.hostControl_opsiclientdRpc( 87 | self, method, params or [], hostIds or [], timeout 88 | ) 89 | 90 | def hostControlSafe_reachable( 91 | self, hostIds: list[str] = None, timeout: int = None 92 | ) -> dict[str, Any]: 93 | if not hostIds: 94 | raise BackendMissingDataError("No matching host ids found") 95 | return HostControlBackend.hostControl_reachable(self, hostIds or [], timeout) 96 | 97 | def hostControlSafe_execute( 98 | self, 99 | command: str, 100 | hostIds: list[str] = None, 101 | waitForEnding: bool = True, 102 | captureStderr: bool = True, 103 | encoding: str = None, 104 | timeout: int = 300, 105 | ): 106 | if not hostIds: 107 | raise BackendMissingDataError("No matching host ids found") 108 | return HostControlBackend.hostControl_execute( 109 | self, 110 | command, 111 | hostIds or [], 112 | waitForEnding, 113 | captureStderr, 114 | encoding, 115 | timeout, 116 | ) 117 | -------------------------------------------------------------------------------- /OPSI/Backend/JSONRPC.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | JSONRPC backend. 7 | 8 | This backend executes the calls on a remote backend via JSONRPC. 9 | """ 10 | 11 | from __future__ import annotations 12 | 13 | from threading import Event 14 | from typing import Any 15 | from urllib.parse import urlparse 16 | 17 | from opsicommon.client.opsiservice import ServiceClient, ServiceConnectionListener 18 | from opsicommon.logging import get_logger 19 | 20 | from OPSI import __version__ 21 | from OPSI.Backend.Base import Backend 22 | 23 | logger = get_logger("opsi.general") 24 | 25 | __all__ = ("JSONRPCBackend",) 26 | 27 | 28 | class JSONRPCBackend(Backend, ServiceConnectionListener): 29 | """ 30 | This Backend gives remote access to a Backend reachable via jsonrpc. 31 | """ 32 | 33 | def __init__(self, address: str, **kwargs: Any) -> None: 34 | """ 35 | Backend for JSON-RPC access to another opsi service. 36 | 37 | :param compression: Should requests be compressed? 38 | :type compression: bool 39 | """ 40 | 41 | self._name = "jsonrpc" 42 | self._connection_result_event = Event() 43 | self._connection_error: Exception | None = None 44 | 45 | Backend.__init__(self, **kwargs) # type: ignore[misc] 46 | 47 | connect_on_init = True 48 | service_args = { 49 | "address": address, 50 | "user_agent": f"opsi-jsonrpc-backend/{__version__}", 51 | "verify": "accept_all", 52 | "jsonrpc_create_objects": True, 53 | "jsonrpc_create_methods": True, 54 | } 55 | for option, value in kwargs.items(): 56 | option = option.lower().replace("_", "") 57 | if option == "username": 58 | service_args["username"] = str(value or "") 59 | elif option == "password": 60 | service_args["password"] = str(value or "") 61 | elif option == "cacertfile": 62 | if value not in (None, ""): 63 | service_args["ca_cert_file"] = str(value) 64 | elif option == "verifyservercert": 65 | if value: 66 | service_args["verify"] = ["opsi_ca", "uib_opsi_ca"] 67 | else: 68 | service_args["verify"] = "accept_all" 69 | elif option == "verify": 70 | service_args["verify"] = value 71 | elif option == "sessionid": 72 | if value: 73 | service_args["session_cookie"] = str(value) 74 | elif option == "sessionlifetime": 75 | if value: 76 | service_args["session_lifetime"] = int(value) 77 | elif option == "proxyurl": 78 | service_args["proxy_url"] = str(value) if value else None 79 | elif option == "application": 80 | service_args["user_agent"] = str(value) 81 | elif option == "connecttimeout": 82 | service_args["connect_timeout"] = int(value) 83 | elif option == "connectoninit": 84 | connect_on_init = bool(value) 85 | 86 | self.service = ServiceClient(**service_args) 87 | self.service.register_connection_listener(self) 88 | if connect_on_init: 89 | self.service.connect() 90 | self._connection_result_event.wait() 91 | if self._connection_error: 92 | raise self._connection_error 93 | 94 | @property 95 | def hostname(self) -> str: 96 | return urlparse(self.service.base_url).hostname 97 | 98 | def connection_established(self, service_client: ServiceClient) -> None: 99 | self.service.create_jsonrpc_methods(self) 100 | self._connection_error = None 101 | self._connection_result_event.set() 102 | 103 | def connection_failed( 104 | self, service_client: ServiceClient, exception: Exception 105 | ) -> None: 106 | self._connection_error = exception 107 | self._connection_result_event.set() 108 | -------------------------------------------------------------------------------- /OPSI/Backend/Manager/Authentication/NT.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | PAM authentication. 7 | """ 8 | 9 | from typing import Set 10 | 11 | # pyright: reportMissingImports=false 12 | import win32net 13 | import win32security 14 | from opsicommon.logging import get_logger 15 | 16 | from OPSI.Backend.Manager.Authentication import AuthenticationModule 17 | from OPSI.Config import OPSI_ADMIN_GROUP 18 | from OPSI.Exceptions import BackendAuthenticationError 19 | 20 | logger = get_logger("opsi.general") 21 | 22 | 23 | class NTAuthentication(AuthenticationModule): 24 | def __init__(self, admin_group_sid: str = None): 25 | super().__init__() 26 | self._admin_group_sid = admin_group_sid 27 | self._admin_groupname = OPSI_ADMIN_GROUP 28 | if self._admin_group_sid is not None: 29 | try: 30 | self._admin_groupname = win32security.LookupAccountSid( 31 | None, win32security.ConvertStringSidToSid(self._admin_group_sid) 32 | )[0] 33 | except Exception as err: 34 | logger.error( 35 | "Failed to lookup group with sid '%s': %s", 36 | self._admin_group_sid, 37 | err, 38 | ) 39 | 40 | def get_instance(self): 41 | return NTAuthentication(self._admin_group_sid) 42 | 43 | def authenticate(self, username: str, password: str) -> None: 44 | """ 45 | Authenticate a user by Windows-Login on current machine 46 | 47 | :raises BackendAuthenticationError: If authentication fails. 48 | """ 49 | logger.confidential( 50 | "Trying to authenticate user %s with password %s by win32security", 51 | username, 52 | password, 53 | ) 54 | 55 | try: 56 | win32security.LogonUser( 57 | username, 58 | "None", 59 | password, 60 | win32security.LOGON32_LOGON_NETWORK, 61 | win32security.LOGON32_PROVIDER_DEFAULT, 62 | ) 63 | except Exception as err: 64 | raise BackendAuthenticationError( 65 | f"Win32security authentication failed for user '{username}': {err}" 66 | ) from err 67 | 68 | def get_admin_groupname(self) -> str: 69 | return self._admin_groupname.lower() 70 | 71 | def get_groupnames(self, username: str) -> Set[str]: 72 | """ 73 | Read the groups of a user. 74 | 75 | :returns: List og group names the user is a member of. 76 | :rtype: set() 77 | """ 78 | collected_groupnames = set() 79 | 80 | gresume = 0 81 | while True: 82 | (groups, gtotal, gresume) = win32net.NetLocalGroupEnum(None, 0, gresume) 83 | logger.trace( 84 | "Got %s groups, total=%s, resume=%s", len(groups), gtotal, gresume 85 | ) 86 | for groupname in (u["name"] for u in groups): 87 | logger.trace("Found group '%s'", groupname) 88 | uresume = 0 89 | while True: 90 | (users, utotal, uresume) = win32net.NetLocalGroupGetMembers( 91 | None, groupname, 0, uresume 92 | ) 93 | logger.trace( 94 | "Got %s users, total=%s, resume=%s", len(users), utotal, uresume 95 | ) 96 | for sid in (u["sid"] for u in users): 97 | (group_username, _domain, _group_type) = ( 98 | win32security.LookupAccountSid(None, sid) 99 | ) 100 | if group_username.lower() == username.lower(): 101 | collected_groupnames.add(groupname) 102 | logger.debug( 103 | "User %s is member of group %s", username, groupname 104 | ) 105 | if uresume == 0: 106 | break 107 | if gresume == 0: 108 | break 109 | 110 | return {g.lower() for g in collected_groupnames} 111 | -------------------------------------------------------------------------------- /OPSI/Backend/Manager/Authentication/PAM.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | PAM authentication. 7 | """ 8 | 9 | import grp 10 | import os 11 | import pwd 12 | from typing import Set 13 | 14 | import pam 15 | from opsicommon.logging import get_logger 16 | 17 | from OPSI.Backend.Manager.Authentication import AuthenticationModule 18 | from OPSI.Exceptions import BackendAuthenticationError 19 | from OPSI.System.Posix import isCentOS, isOpenSUSE, isRHEL, isSLES 20 | 21 | logger = get_logger("opsi.general") 22 | 23 | 24 | class PAMAuthentication(AuthenticationModule): 25 | def __init__(self, pam_service: str = None): 26 | super().__init__() 27 | self._pam_service = pam_service 28 | if not self._pam_service: 29 | if os.path.exists("/etc/pam.d/opsi-auth"): 30 | # Prefering our own - if present. 31 | self._pam_service = "opsi-auth" 32 | elif isSLES() or isOpenSUSE(): 33 | self._pam_service = "sshd" 34 | elif isCentOS() or isRHEL(): 35 | self._pam_service = "system-auth" 36 | else: 37 | self._pam_service = "common-auth" 38 | 39 | def get_instance(self): 40 | return PAMAuthentication(self._pam_service) 41 | 42 | def authenticate(self, username: str, password: str) -> None: 43 | """ 44 | Authenticate a user by PAM (Pluggable Authentication Modules). 45 | Important: the uid running this code needs access to /etc/shadow 46 | if os uses traditional unix authentication mechanisms. 47 | 48 | :param service: The PAM service to use. Leave None for autodetection. 49 | :type service: str 50 | :raises BackendAuthenticationError: If authentication fails. 51 | """ 52 | logger.confidential( 53 | "Trying to authenticate user %s with password %s by PAM", username, password 54 | ) 55 | logger.trace( 56 | "Attempting PAM authentication as user %s (service=%s)...", 57 | username, 58 | self._pam_service, 59 | ) 60 | 61 | try: 62 | auth = pam.pam() 63 | if not auth.authenticate(username, password, service=self._pam_service): 64 | logger.trace( 65 | "PAM authentication failed: %s (code %s)", auth.reason, auth.code 66 | ) 67 | raise RuntimeError(auth.reason) 68 | 69 | logger.trace("PAM authentication successful.") 70 | except Exception as err: 71 | raise BackendAuthenticationError( 72 | f"PAM authentication failed for user '{username}': {err}" 73 | ) from err 74 | 75 | def get_groupnames(self, username: str) -> Set[str]: 76 | """ 77 | Read the groups of a user. 78 | 79 | :returns: Group the user is a member of. 80 | :rtype: set() 81 | """ 82 | logger.debug("Getting groups of user %s", username) 83 | primary_gid = pwd.getpwnam(username).pw_gid 84 | logger.debug("Primary group id of user %s is %s", username, primary_gid) 85 | groups = set() 86 | for gid in os.getgrouplist(username, primary_gid): 87 | try: 88 | groups.add(grp.getgrgid(gid).gr_name) 89 | except KeyError as err: 90 | logger.warning(err) 91 | logger.debug("User %s is member of groups: %s", username, groups) 92 | return {g.lower() for g in groups} 93 | -------------------------------------------------------------------------------- /OPSI/Backend/Manager/Authentication/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Authentication helper. 7 | """ 8 | 9 | from typing import Set 10 | 11 | from opsicommon.logging import get_logger 12 | 13 | from OPSI.Config import OPSI_ADMIN_GROUP 14 | from OPSI.Exceptions import BackendAuthenticationError 15 | from OPSI.Util.File.Opsi import OpsiConfFile 16 | 17 | logger = get_logger("opsi.general") 18 | 19 | 20 | class AuthenticationModule: 21 | def __init__(self): 22 | pass 23 | 24 | def get_instance(self): 25 | return self.__class__() 26 | 27 | def authenticate(self, username: str, password: str) -> None: 28 | raise BackendAuthenticationError("Not implemented") 29 | 30 | def get_groupnames(self, username: str) -> Set[str]: 31 | return set() 32 | 33 | def get_admin_groupname(self) -> str: 34 | return OPSI_ADMIN_GROUP 35 | 36 | def get_read_only_groupnames(self) -> Set[str]: 37 | return set(OpsiConfFile().getOpsiGroups("readonly") or []) 38 | 39 | def user_is_admin(self, username: str) -> bool: 40 | return self.get_admin_groupname() in self.get_groupnames(username) 41 | 42 | def user_is_read_only( 43 | self, username: str, forced_user_groupnames: Set[str] = None 44 | ) -> bool: 45 | user_groupnames = set() 46 | if forced_user_groupnames is None: 47 | user_groupnames = self.get_groupnames(username) 48 | else: 49 | user_groupnames = forced_user_groupnames 50 | 51 | read_only_groupnames = self.get_read_only_groupnames() 52 | for group_name in user_groupnames: 53 | if group_name in read_only_groupnames: 54 | return True 55 | return False 56 | -------------------------------------------------------------------------------- /OPSI/Backend/Manager/Config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | BackendManager configuration helper. 7 | """ 8 | 9 | import os 10 | import socket 11 | import sys 12 | from functools import lru_cache 13 | 14 | from OPSI.Exceptions import BackendConfigurationError 15 | 16 | 17 | def loadBackendConfig(path): 18 | """ 19 | Load the backend configuration at `path`. 20 | :param path: Path to the configuration file to load. 21 | :type path: str 22 | :rtype: dict 23 | """ 24 | if not os.path.exists(path): 25 | raise BackendConfigurationError(f"Backend config file '{path}' not found") 26 | 27 | moduleGlobals = { 28 | "config": {}, # Will be filled after loading 29 | "module": "", # Will be filled after loading 30 | "os": os, 31 | "socket": socket, 32 | "sys": sys, 33 | } 34 | 35 | exec(_readFile(path), moduleGlobals) 36 | 37 | return moduleGlobals 38 | 39 | 40 | @lru_cache(maxsize=None) 41 | def _readFile(path): 42 | with open(path, encoding="utf-8") as configFile: 43 | return configFile.read() 44 | -------------------------------------------------------------------------------- /OPSI/Backend/Manager/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Backend managing components. 7 | """ 8 | -------------------------------------------------------------------------------- /OPSI/Backend/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Backends. 7 | """ 8 | 9 | import functools 10 | from typing import Callable 11 | 12 | 13 | def no_export(func: Callable) -> Callable: 14 | func.no_export = True 15 | return func 16 | 17 | 18 | def deprecated( 19 | func: Callable = None, *, alternative_method: Callable = None 20 | ) -> Callable: 21 | if func is None: 22 | return functools.partial(deprecated, alternative_method=alternative_method) 23 | 24 | func.deprecated = True 25 | func.alternative_method = alternative_method 26 | return func 27 | 28 | # @functools.wraps(func) 29 | # def wrapper(*args, **kwargs): 30 | # logger.warning("Deprecated") 31 | # return func(*args, **kwargs) 32 | # return wrapper 33 | -------------------------------------------------------------------------------- /OPSI/Config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Various important configuration values. 7 | 8 | This module should be used to refer to often used values in a consistent 9 | way instead of hardcoding the values. 10 | 11 | If new values are added they must be added that the module stays 12 | functional independen of the current underlying system. 13 | 14 | These values are not intended to be changed on-the-fly! 15 | Doing so might result in unforseen problems and is strongly discouraged! 16 | """ 17 | 18 | # Group used to identify members whits administrative rights in opsi 19 | OPSI_ADMIN_GROUP = "opsiadmin" 20 | 21 | # Default user when accessing the opsi depot 22 | DEFAULT_DEPOT_USER = "pcpatch" 23 | 24 | 25 | # Default home dir of depot user 26 | DEFAULT_DEPOT_USER_HOME = "/var/lib/opsi" 27 | 28 | # Path to global opsi configuration file 29 | OPSI_GLOBAL_CONF = "/etc/opsi/global.conf" 30 | 31 | try: 32 | from OPSI.Util.File.Opsi import OpsiConfFile 33 | 34 | OPSI_ADMIN_GROUP = OpsiConfFile().getOpsiAdminGroup() 35 | FILE_ADMIN_GROUP = OpsiConfFile().getOpsiFileAdminGroup() 36 | except Exception: 37 | # Use "pcpatch" if group exists otherwise use the new default "opsifileadmins" 38 | try: 39 | import grp 40 | 41 | grp.getgrnam("pcpatch") 42 | FILE_ADMIN_GROUP = "pcpatch" 43 | except (KeyError, ImportError): 44 | FILE_ADMIN_GROUP = "opsifileadmins" 45 | 46 | # User that is running opsiconfd. 47 | try: 48 | # pyright: reportMissingImports=false 49 | from opsiconfd.config import config 50 | 51 | OPSICONFD_USER = config.run_as_user 52 | except Exception: 53 | OPSICONFD_USER = "opsiconfd" 54 | -------------------------------------------------------------------------------- /OPSI/Exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | OPSI Exceptions. 7 | Deprecated, use opsicommon.exceptions instead. 8 | """ 9 | 10 | from opsicommon.exceptions import * # noqa: F403 11 | 12 | 13 | class CommandNotFoundException(RuntimeError): 14 | pass 15 | -------------------------------------------------------------------------------- /OPSI/Object.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | General classes used in the library. 7 | 8 | As an example this contains classes for hosts, products, configurations. 9 | 10 | Deprecated, use opsicommon.objects instead. 11 | """ 12 | 13 | from typing import Any 14 | 15 | from opsicommon.objects import * # noqa: F403 16 | 17 | mandatoryConstructorArgs = mandatory_constructor_args 18 | getIdentAttributes = get_ident_attributes 19 | getForeignIdAttributes = get_foreign_id_attributes 20 | getBackendMethodPrefix = get_backend_method_prefix 21 | getPossibleClassAttributes = get_possible_class_attributes 22 | decodeIdent = decode_ident 23 | 24 | 25 | def objectsDiffer(obj1: Any, obj2: Any, excludeAttributes: list[str] = None) -> bool: 26 | return objects_differ(obj1, obj2, exclude_attributes=excludeAttributes) 27 | -------------------------------------------------------------------------------- /OPSI/Types.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Type forcing features. 7 | 8 | This module contains various methods to ensure force a special type 9 | on an object. 10 | Deprecated, use opsicommon.types instead. 11 | """ 12 | 13 | from opsicommon.types import * # noqa: F403 14 | -------------------------------------------------------------------------------- /OPSI/Util/File/Opsi/Opsirc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Handling an .opsirc file. 7 | 8 | An .opsirc file contains information about how to connect to an 9 | opsi API. 10 | By default the file is expected to be at `~/.opsi.org/opsirc`. 11 | 12 | An example:: 13 | 14 | address = https://opsimain.domain.local:4447/rpc 15 | username = myname 16 | password = topsecret 17 | 18 | 19 | None of these settings are mandatory. 20 | 21 | Instead of writing the password directly to the file it is possible 22 | to reference a file with the secret as follows:: 23 | 24 | password file = ~/.opsi.org/opsirc.secret 25 | 26 | 27 | The files should be encoded as utf-8. 28 | """ 29 | 30 | import codecs 31 | import os 32 | 33 | from opsicommon.logging import get_logger, secret_filter 34 | 35 | from OPSI.Types import forceUnicode, forceUrl 36 | 37 | __all__ = ("getOpsircPath", "readOpsirc") 38 | 39 | logger = get_logger("opsi.general") 40 | 41 | 42 | def readOpsirc(filename=None): 43 | """ 44 | Read the configuration file and parse it for usable information. 45 | 46 | :param filename: The path of the file to read. Defaults to using \ 47 | the result from `getOpsircPath`. 48 | :type filename: str 49 | :returns: Settings read from the file. Possible keys are `username`,\ 50 | `password` and `address`. 51 | :rtype: {str: str} 52 | """ 53 | if filename is None: 54 | filename = getOpsircPath() 55 | 56 | if not os.path.exists(filename): 57 | logger.debug(".opsirc file %s does not exist.", filename) 58 | return {} 59 | 60 | return _parseConfig(filename) 61 | 62 | 63 | def getOpsircPath(): 64 | """ 65 | Return the path where an opsirc file is expected to be. 66 | 67 | :return: The path of an opsirc file. 68 | :rtype: str 69 | """ 70 | path = os.path.expanduser("~/.opsi.org/opsirc") 71 | return path 72 | 73 | 74 | def _parseConfig(filename): 75 | config = {} 76 | with codecs.open(filename, mode="r", encoding="utf-8") as opsircfile: 77 | for line in opsircfile: 78 | line = line.strip() 79 | if line.startswith(("#", ";")) or not line: 80 | continue 81 | 82 | try: 83 | key, value = line.split("=", 1) 84 | except ValueError: 85 | logger.trace("Unable to split line %s", line) 86 | continue 87 | 88 | key = key.strip() 89 | value = value.strip() 90 | 91 | if not value: 92 | logger.warning( 93 | "There is no value for %s in opsirc file %s, skipping.", 94 | key, 95 | filename, 96 | ) 97 | continue 98 | 99 | if key == "address": 100 | config[key] = forceUrl(value) 101 | elif key == "username": 102 | config[key] = forceUnicode(value) 103 | elif key == "password": 104 | value = forceUnicode(value) 105 | secret_filter.add_secrets(value) 106 | config[key] = value 107 | elif key == "password file": 108 | passwordFilePath = os.path.expanduser(value) 109 | value = _readPasswordFile(passwordFilePath) 110 | secret_filter.add_secrets(value) 111 | config["password"] = value 112 | else: 113 | logger.debug("Ignoring unknown key %s", key) 114 | 115 | logger.debug( 116 | "Found the following usable keys in %s: %s", 117 | filename, 118 | ", ".join(list(config.keys())), 119 | ) 120 | return config 121 | 122 | 123 | def _readPasswordFile(filename): 124 | with codecs.open(filename, mode="r", encoding="utf-8") as pwfile: 125 | password = pwfile.read() 126 | 127 | return password.strip() 128 | -------------------------------------------------------------------------------- /OPSI/Util/Log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Utilities for working with logs. 7 | """ 8 | 9 | from OPSI.Types import forceInt 10 | 11 | __all__ = ("truncateLogData",) 12 | 13 | 14 | def truncateLogData(data, maxSize): 15 | """ 16 | Truncating `data` to not be longer than `maxSize` chars. 17 | 18 | :param data: Text 19 | :type data: str 20 | :param maxSize: The maximum size that is allowed in chars. 21 | :type maxSize: int 22 | """ 23 | maxSize = forceInt(maxSize) 24 | dataLength = len(data) 25 | if dataLength > maxSize: 26 | start = data.find("\n", dataLength - maxSize) 27 | if start == -1: 28 | start = dataLength - maxSize 29 | return data[start:].lstrip() 30 | 31 | return data 32 | -------------------------------------------------------------------------------- /OPSI/Util/Path.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Functionality to work with paths. 7 | """ 8 | 9 | import os 10 | from contextlib import contextmanager 11 | 12 | 13 | @contextmanager 14 | def cd(path: str): 15 | "Change the current directory to `path` as long as the context exists." 16 | 17 | currentDir = os.getcwd() 18 | os.chdir(path) 19 | try: 20 | yield 21 | finally: 22 | os.chdir(currentDir) 23 | -------------------------------------------------------------------------------- /OPSI/Util/Task/ConfigureBackend/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Functionality to automatically configure an OPSI backend. 7 | 8 | .. versionadded:: 4.0.4.6 9 | """ 10 | 11 | import codecs 12 | import os 13 | import re 14 | import socket 15 | import sys 16 | 17 | from opsicommon.logging import get_logger 18 | 19 | from OPSI.System.Posix import getLocalFqdn, getNetworkConfiguration 20 | from OPSI.Util import objectToBeautifiedText 21 | 22 | __all__ = ("getBackendConfiguration", "updateConfigFile") 23 | 24 | logger = get_logger("opsi.general") 25 | 26 | 27 | def getBackendConfiguration(backendConfigFile, customGlobals=None): 28 | """ 29 | Reads the backend configuration from the given file. 30 | 31 | :param backendConfigFile: Path to the backend configuration file. 32 | :param customGlobals: If special locals are needed for the config file \ 33 | please pass them here. If this is None defaults will be used. 34 | :type customGlobals: dict 35 | """ 36 | if customGlobals is None: 37 | customGlobals = { 38 | "config": {}, # Will be filled after loading 39 | "module": "", # Will be filled after loading 40 | "os": os, 41 | "socket": socket, 42 | "sys": sys, 43 | } 44 | 45 | logger.info("Loading backend config '%s'", backendConfigFile) 46 | with open(backendConfigFile, encoding="utf-8") as configFile: 47 | exec(configFile.read(), customGlobals) 48 | 49 | config = customGlobals["config"] 50 | logger.debug("Current backend config: %s", config) 51 | 52 | return config 53 | 54 | 55 | def updateConfigFile(backendConfigFile, newConfig, notificationFunction=None): 56 | """ 57 | Updates a config file with the corresponding new configuration. 58 | 59 | :param backendConfigFile: path to the backend configuration 60 | :param newConfig: the new configuration. 61 | :param notificationFunction: A function that log messages will be passed \ 62 | on to. Defaults to logger.notice 63 | :type notificationFunction: func 64 | """ 65 | 66 | def correctBooleans(text): 67 | """ 68 | Creating correct JSON booleans - they are all lowercase. 69 | """ 70 | return text.replace("true", "True").replace("false", "False") 71 | 72 | if notificationFunction is None: 73 | notificationFunction = logger.notice 74 | 75 | notificationFunction(f"Updating backend config '{backendConfigFile}'") 76 | 77 | lines = [] 78 | with codecs.open(backendConfigFile, "r", "utf-8") as backendFile: 79 | for line in backendFile.readlines(): 80 | if re.search(r"^\s*config\s*\=", line): 81 | break 82 | lines.append(line) 83 | 84 | with codecs.open(backendConfigFile, "w", "utf-8") as backendFile: 85 | backendFile.writelines(lines) 86 | backendConfigData = correctBooleans(objectToBeautifiedText(newConfig)) 87 | backendFile.write(f"config = {backendConfigData}\n") 88 | 89 | notificationFunction(f"Backend config '{backendConfigFile}' updated") 90 | 91 | 92 | def _getSysConfig(): 93 | """ 94 | Skinned down version of getSysConfig from ``opsi-setup``. 95 | 96 | Should be used as **fallback only**! 97 | """ 98 | logger.notice("Getting current system config") 99 | fqdn = getLocalFqdn() 100 | sysConfig = {"fqdn": fqdn, "hostname": fqdn.split(".")[0]} 101 | 102 | sysConfig.update(getNetworkConfiguration()) 103 | 104 | logger.notice("System information:") 105 | logger.notice(" ip address : %s", sysConfig["ipAddress"]) 106 | logger.notice(" netmask : %s", sysConfig["netmask"]) 107 | logger.notice(" subnet : %s", sysConfig["subnet"]) 108 | logger.notice(" broadcast : %s", sysConfig["broadcast"]) 109 | logger.notice(" fqdn : %s", sysConfig["fqdn"]) 110 | logger.notice(" hostname : %s", sysConfig["hostname"]) 111 | 112 | return sysConfig 113 | -------------------------------------------------------------------------------- /OPSI/Util/Task/UpdateBackend/ConfigurationData.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Updating backend data. 7 | 8 | This holds backend-independent migrations. 9 | """ 10 | 11 | from opsicommon.logging import get_logger 12 | 13 | from OPSI.System.Posix import isOpenSUSE, isSLES 14 | 15 | __all__ = ("updateBackendData",) 16 | 17 | logger = get_logger("opsi.general") 18 | 19 | 20 | def updateBackendData(backend): 21 | setDefaultWorkbenchLocation(backend) 22 | 23 | 24 | def setDefaultWorkbenchLocation(backend): 25 | """ 26 | Set the possibly missing workbench location on the server. 27 | 28 | The value is regarded as missing if it is not set to None. 29 | `workbenchLocalUrl` will be set to `file:///var/lib/opsi/workbench` 30 | on SUSE system and to `file:///home/opsiproducts` on others. 31 | `workbenchRemoteUrl` will use the same value for the depot address 32 | that is set in `depotRemoteUrl` and then will point to the samba 33 | share _opsi_workbench_. 34 | """ 35 | servers = backend.host_getObjects(type=["OpsiDepotserver", "OpsiConfigserver"]) 36 | 37 | if isSLES() or isOpenSUSE(): 38 | # On Suse 39 | localWorkbenchPath = "file:///var/lib/opsi/workbench" 40 | else: 41 | # On non-SUSE systems the path was usually /home/opsiproducts 42 | localWorkbenchPath = "file:///home/opsiproducts" 43 | 44 | changedServers = set() 45 | for server in servers: 46 | if server.getWorkbenchLocalUrl() is None: 47 | logger.notice( 48 | "Setting missing value for workbenchLocalUrl on %s to %s", 49 | server.id, 50 | localWorkbenchPath, 51 | ) 52 | server.setWorkbenchLocalUrl(localWorkbenchPath) 53 | changedServers.add(server) 54 | 55 | if server.getWorkbenchRemoteUrl() is None: 56 | depotAddress = getServerAddress(server.depotRemoteUrl) 57 | remoteWorkbenchPath = f"smb://{depotAddress}/opsi_workbench" 58 | logger.notice( 59 | "Setting missing value for workbenchRemoteUrl on %s to %s", 60 | server.id, 61 | remoteWorkbenchPath, 62 | ) 63 | server.setWorkbenchRemoteUrl(remoteWorkbenchPath) 64 | changedServers.add(server) 65 | 66 | if changedServers: 67 | backend.host_updateObjects(changedServers) 68 | 69 | 70 | def getServerAddress(depotRemoteUrl): 71 | """ 72 | Get the address of the server from the `depotRemoteUrl`. 73 | 74 | :param depotRemoteUrl: the depotRemoteUrl of an OpsiDepotserver 75 | :type depotRemoteUrl: str 76 | :rtype: str 77 | """ 78 | _, addressAndPath = depotRemoteUrl.split(":") 79 | return addressAndPath.split("/")[2] 80 | -------------------------------------------------------------------------------- /OPSI/Util/Task/UpdateBackend/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Functionality to update OPSI backends. 7 | 8 | .. versionadded:: 4.0.6.1 9 | """ 10 | 11 | 12 | class BackendUpdateError(RuntimeError): 13 | "This error indicates a problem during a backend update." 14 | -------------------------------------------------------------------------------- /OPSI/Util/Task/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | opsi python library - Util - Task 7 | 8 | This module is part of the desktop management solution opsi 9 | (open pc server integration) http://www.opsi.org 10 | 11 | Copyright (C) 2006-2019 uib GmbH 12 | 13 | http://www.uib.de/ 14 | 15 | All rights reserved. 16 | 17 | This program is free software; you can redistribute it and/or modify 18 | it under the terms of the GNU General Public License version 2 as 19 | published by the Free Software Foundation. 20 | 21 | This program is distributed in the hope that it will be useful, 22 | but WITHOUT ANY WARRANTY; without even the implied warranty of 23 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 | GNU General Public License for more details. 25 | 26 | You should have received a copy of the GNU General Public License 27 | along with this program; if not, write to the Free Software 28 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 29 | 30 | @copyright: uib GmbH 31 | @author: Christian Kampka 32 | @license: GNU General Public License version 2 33 | """ 34 | -------------------------------------------------------------------------------- /OPSI/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | opsi python library. 7 | 8 | This module is part of the desktop management solution opsi 9 | (open pc server integration) http://www.opsi.org 10 | """ 11 | 12 | __version__ = "4.3.0.28" 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /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_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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /opsi-dev-tool.yml: -------------------------------------------------------------------------------- 1 | locale: 2 | languages: 3 | - de 4 | - da 5 | - fr 6 | source_files: 7 | - OPSI/UI.py 8 | install: python-opsi_data/locale 9 | 10 | package: 11 | name: python3-opsi 12 | type: poetry 13 | depends: 14 | - python3-magic 15 | - python3-pampy 16 | - python3-pyasn1 17 | - python3-pycryptodome 18 | - python3-sqlalchemy 19 | - python3-mysqldb 20 | - python3-distro 21 | - python3-newt 22 | - python3-psutil 23 | - librsync | librsync2 | librsync1 24 | - iproute2 25 | - lshw 26 | 27 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | 4 | -------------------------------------------------------------------------------- /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.9.5" 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 | 49 | [tool.ruff.format] 50 | indent-style = "tab" 51 | 52 | [tool.ruff.lint] 53 | ignore = [ "F405",] 54 | 55 | [tool.poetry.dependencies.distro] 56 | platform = "linux" 57 | version = ">=1.5" 58 | 59 | [tool.poetry.dependencies.pywin32] 60 | platform = "win32" 61 | version = ">=303" 62 | 63 | [tool.poetry.dependencies.wmi] 64 | platform = "win32" 65 | version = ">=1.5" 66 | 67 | [tool.poetry.group.dev.dependencies] 68 | mock = ">=5.0" 69 | pytest = ">=8.3" 70 | pytest-asyncio = ">=0.24" 71 | pytest-cov = ">=5.0" 72 | ruff = ">=0.7" 73 | -------------------------------------------------------------------------------- /tests/Backends/File.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Mixins that provide an ready to use File backend. 7 | """ 8 | 9 | import grp 10 | import os 11 | import pwd 12 | import shutil 13 | import tempfile 14 | from contextlib import contextmanager 15 | 16 | from OPSI.Backend.Backend import ExtendedConfigDataBackend 17 | from OPSI.Backend.File import FileBackend 18 | 19 | from ..helpers import workInTemporaryDirectory 20 | from . import BackendMixin 21 | 22 | 23 | class FileBackendMixin(BackendMixin): 24 | BACKEND_SUBFOLDER = os.path.join("etc", "opsi") 25 | CONFIG_DIRECTORY = os.path.join("var", "lib", "opsi") 26 | CREATES_INVENTORY_HISTORY = False 27 | 28 | def setUpBackend(self): 29 | self._fileBackendConfig = {} 30 | self._fileTempDir = self._copyOriginalBackendToTemporaryLocation() 31 | 32 | self.backend = ExtendedConfigDataBackend(FileBackend(**self._fileBackendConfig)) 33 | self.backend.backend_createBase() 34 | 35 | def _copyOriginalBackendToTemporaryLocation(self): 36 | tempDir = tempfile.mkdtemp() 37 | originalBackendDir = _getOriginalBackendLocation() 38 | 39 | shutil.copytree( 40 | originalBackendDir, os.path.join(tempDir, self.BACKEND_SUBFOLDER) 41 | ) 42 | 43 | self._setupFileBackend(tempDir) 44 | self._patchDispatchConfig(tempDir) 45 | 46 | return tempDir 47 | 48 | def _setupFileBackend(self, targetDirectory): 49 | self._patchFileBackend(targetDirectory) 50 | self._createClientTemplateFolders( 51 | os.path.join(targetDirectory, self.CONFIG_DIRECTORY) 52 | ) 53 | 54 | def _patchFileBackend(self, backendDirectory): 55 | baseDir = os.path.join(backendDirectory, self.CONFIG_DIRECTORY, "config") 56 | hostKeyDir = os.path.join(backendDirectory, self.BACKEND_SUBFOLDER, "pckeys") 57 | 58 | currentGroupId = os.getgid() 59 | groupName = grp.getgrgid(currentGroupId)[0] 60 | 61 | userName = pwd.getpwuid(os.getuid())[0] 62 | 63 | self._fileBackendConfig.update( 64 | dict( 65 | basedir=baseDir, 66 | hostKeyFile=hostKeyDir, 67 | fileGroupName=groupName, 68 | fileUserName=userName, 69 | ) 70 | ) 71 | 72 | config_file = os.path.join( 73 | backendDirectory, self.BACKEND_SUBFOLDER, "backends", "file.conf" 74 | ) 75 | with open(config_file, "w") as config: 76 | new_configuration = """ 77 | # -*- coding: utf-8 -*- 78 | 79 | module = 'File' 80 | config = {{ 81 | "baseDir": "{basedir}", 82 | "hostKeyFile": "{keydir}", 83 | "fileGroupName": "{groupName}", 84 | "fileUserName": "{userName}", 85 | }} 86 | """.format(basedir=baseDir, keydir=hostKeyDir, groupName=groupName, userName=userName) 87 | 88 | config.write(new_configuration) 89 | 90 | @classmethod 91 | def _createClientTemplateFolders(cls, targetDirectory): 92 | templateDirectory = os.path.join(targetDirectory, "config", "templates") 93 | os.makedirs(templateDirectory) 94 | 95 | def _patchDispatchConfig(self, targetDirectory): 96 | configDir = os.path.join(targetDirectory, self.BACKEND_SUBFOLDER, "backends") 97 | dispatchConfigPath = os.path.join(configDir, "dispatch.conf") 98 | 99 | self._fileBackendConfig["dispatchConfig"] = dispatchConfigPath 100 | 101 | with open(dispatchConfigPath, "w") as dpconf: 102 | dpconf.write(""" 103 | .* : file 104 | """) 105 | 106 | def tearDownBackend(self): 107 | self.backend.backend_deleteBase() 108 | 109 | try: 110 | shutil.rmtree(self._fileTempDir) 111 | except OSError: 112 | pass 113 | 114 | del self.backend 115 | 116 | 117 | @contextmanager 118 | def getFileBackend(path=None, **backendOptions): 119 | originalLocation = _getOriginalBackendLocation() 120 | 121 | BACKEND_SUBFOLDER = os.path.join("etc", "opsi") 122 | CONFIG_DIRECTORY = os.path.join("var", "lib", "opsi") 123 | 124 | with workInTemporaryDirectory(path) as tempDir: 125 | shutil.copytree(originalLocation, os.path.join(tempDir, BACKEND_SUBFOLDER)) 126 | 127 | baseDir = os.path.join(tempDir, CONFIG_DIRECTORY, "config") 128 | os.makedirs(baseDir) # Usually done in OS package 129 | hostKeyFile = os.path.join(tempDir, BACKEND_SUBFOLDER, "pckeys") 130 | 131 | currentGroupId = os.getgid() 132 | groupName = grp.getgrgid(currentGroupId)[0] 133 | 134 | userName = pwd.getpwuid(os.getuid())[0] 135 | 136 | backendConfig = { 137 | "baseDir": baseDir, 138 | "hostKeyFile": hostKeyFile, 139 | "fileGroupName": groupName, 140 | "fileUserName": userName, 141 | } 142 | backendConfig.update(backendOptions) 143 | 144 | new_configuration = """ 145 | # -*- coding: utf-8 -*- 146 | 147 | module = 'File' 148 | config = {{ 149 | "baseDir": "{baseDir}", 150 | "hostKeyFile": "{hostKeyFile}", 151 | "fileGroupName": "{fileGroupName}", 152 | "fileUserName": "{fileUserName}", 153 | }} 154 | """.format(**backendConfig) 155 | 156 | config_file = os.path.join(tempDir, BACKEND_SUBFOLDER, "backends", "file.conf") 157 | with open(config_file, "w") as config: 158 | config.write(new_configuration) 159 | 160 | yield FileBackend(**backendConfig) 161 | 162 | 163 | def _getOriginalBackendLocation(): 164 | from ..conftest import DIST_DATA_PATH 165 | 166 | return DIST_DATA_PATH 167 | -------------------------------------------------------------------------------- /tests/Backends/MySQL.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | MySQL backend test helpers 7 | """ 8 | 9 | from contextlib import contextmanager 10 | 11 | import pytest 12 | 13 | from OPSI.Backend.MySQL import ( 14 | MySQL, 15 | MySQLBackend, 16 | MySQLBackendObjectModificationTracker, 17 | ) 18 | from OPSI.Util.Task.UpdateBackend.MySQL import disableForeignKeyChecks 19 | 20 | try: 21 | from .config import MySQLconfiguration 22 | except ImportError: 23 | MySQLconfiguration = None 24 | 25 | UNKNOWN_TABLE_ERROR_CODE = 1051 26 | 27 | 28 | @contextmanager 29 | def getMySQLBackend(**backendOptions): 30 | if not MySQLconfiguration: 31 | pytest.skip("no MySQL backend configuration given.") 32 | 33 | optionsForBackend = MySQLconfiguration 34 | optionsForBackend.update(backendOptions) 35 | 36 | with cleanDatabase(MySQL(**optionsForBackend)): 37 | yield MySQLBackend(**optionsForBackend) 38 | 39 | 40 | @contextmanager 41 | def getMySQLModificationTracker(): 42 | if not MySQLconfiguration: 43 | pytest.skip("no MySQL backend configuration given.") 44 | 45 | yield MySQLBackendObjectModificationTracker(**MySQLconfiguration) 46 | 47 | 48 | @contextmanager 49 | def cleanDatabase(database): 50 | def dropAllTables(database): 51 | with database.session() as session: 52 | with disableForeignKeyChecks(database, session): 53 | # Drop database 54 | error_count = 0 55 | success = False 56 | while not success: 57 | success = True 58 | for table_name in getTableNames(database, session): 59 | drop_command = f"DROP TABLE `{table_name}`" 60 | try: 61 | database.execute(session, drop_command) 62 | except Exception: 63 | success = False 64 | error_count += 1 65 | if error_count > 10: 66 | raise 67 | 68 | dropAllTables(database) 69 | try: 70 | yield database 71 | finally: 72 | dropAllTables(database) 73 | 74 | 75 | def getTableNames(database, session): 76 | return set(tuple(i.values())[0] for i in database.getSet(session, "SHOW TABLES")) 77 | -------------------------------------------------------------------------------- /tests/Backends/SQLite.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 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/Backends/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Basics for backend tests. 7 | """ 8 | 9 | from contextlib import contextmanager 10 | 11 | from OPSI.Backend.Backend import ExtendedConfigDataBackend 12 | 13 | __all__ = ("getTestBackend", "BackendMixin") 14 | 15 | 16 | @contextmanager 17 | def getTestBackend(extended=False): 18 | """ 19 | Get a backend for tests. 20 | 21 | Each call to this will return a different backend. 22 | 23 | If `extended` is True the returned backend will be an 24 | `ExtendedConfigDataBackend`. 25 | """ 26 | from .File import getFileBackend # lazy import 27 | 28 | with getFileBackend() as backend: 29 | if extended: 30 | backend = ExtendedConfigDataBackend(backend) 31 | 32 | backend.backend_createBase() 33 | try: 34 | yield backend 35 | finally: 36 | backend.backend_deleteBase() 37 | 38 | 39 | class BackendMixin: 40 | """ 41 | Base class for backend test mixins. 42 | 43 | :param CREATES_INVENTORY_HISTORY: Set to true if the backend keeps a \ 44 | history of the inventory. This will affects tests! 45 | :type CREATES_INVENTORY_HISTORY: bool 46 | """ 47 | 48 | CREATES_INVENTORY_HISTORY = False 49 | 50 | def setUpBackend(self): 51 | pass 52 | 53 | def tearDownBackend(self): 54 | pass 55 | -------------------------------------------------------------------------------- /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/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/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | OPSI Python Library - Testcases. 7 | 8 | Various unittests to test functionality of python-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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!!! -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/davxml/twisted-davxml.data: -------------------------------------------------------------------------------- 1 | 2 | 3 | /repository/ 4 | 5 | 6 | 7 | 8 | 9 | "2-9000-5D1E186F" 10 | httpd/unix-directory 11 | None 12 | Thu, 04 Jul 2019 17:17:03 GMT 13 | 2019-07-04T17:17:03Z 14 | repository 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ResourceOpsiconfdDAV 34 | 35 | HTTP/1.1 200 OK 36 | 37 | 38 | 39 | /repository/l-opsi-server_4.1.1.11-1.opsi.zsync 40 | 41 | 42 | 43 | "20D-139-5D1B4E09" 44 | text/plain 45 | 313 46 | Tue, 02 Jul 2019 14:28:57 GMT 47 | 2019-07-02T14:28:57Z 48 | l-opsi-server_4.1.1.11-1.opsi.zsync 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ResourceOpsiconfdDAV 68 | 69 | HTTP/1.1 200 OK 70 | 71 | 72 | 73 | /repository/l-opsi-server_4.1.1.11-1.opsi.md5 74 | 75 | 76 | 77 | "20A-20-5D1B4E09" 78 | text/plain 79 | 32 80 | Tue, 02 Jul 2019 14:28:57 GMT 81 | 2019-07-02T14:28:57Z 82 | l-opsi-server_4.1.1.11-1.opsi.md5 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | ResourceOpsiconfdDAV 102 | 103 | HTTP/1.1 200 OK 104 | 105 | 106 | 107 | /repository/l-opsi-server_4.1.1.11-2.opsi.zsync 108 | 109 | 110 | 111 | "259-139-5D1E184F" 112 | text/plain 113 | 313 114 | Thu, 04 Jul 2019 17:16:31 GMT 115 | 2019-07-04T17:16:31Z 116 | l-opsi-server_4.1.1.11-2.opsi.zsync 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | ResourceOpsiconfdDAV 136 | 137 | HTTP/1.1 200 OK 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/data/util/dhcpd/link_to_dhcpd1_1.conf: -------------------------------------------------------------------------------- 1 | dhcpd_1.conf -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/data/util/file/opsi-configed_4.0.7.1.3-2.opsi.zsync: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opsi-org/python-opsi-legacy/975b0a751b82af55dafaf2422a2954cc5174e108/tests/data/util/file/opsi-configed_4.0.7.1.3-2.opsi.zsync -------------------------------------------------------------------------------- /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 | """ -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/data/util/file/txtsetupoem_testdata_2.oem: -------------------------------------------------------------------------------- 1 | # txtsetup.oem - version XP.8 for SYMMPI Windows XP driver 2 | # 3 | # *********************************************************************** 4 | # * 5 | # Copyright 2004 LSI Logic, Corp. All rights reserved. * 6 | # * 7 | # This file is property of LSI Logic, Corp. and is licensed for * 8 | # use as is. The receipt of or posession of this file does not * 9 | # convey any rights to modify its contents, in whole, or in part, * 10 | # without the specific written consent of LSI Logic, Corp. * 11 | # * 12 | # *********************************************************************** 13 | # 14 | # format for txtsetup.oem. 15 | # 16 | # General format: 17 | # 18 | # [section] 19 | # key = value1,value2,... 20 | # 21 | # 22 | # The hash ('#') introduces a comment. 23 | # Strings with embedded spaces, commas, or hashes should be double-quoted 24 | # 25 | 26 | 27 | [Disks] 28 | 29 | # This section lists all disks in the disk set. 30 | # 31 | # is a descriptive name for a disk, used when 32 | # prompting for the disk 33 | # is a file whose presence allows setup to recognize 34 | # that the disk is inserted. 35 | # is where the files are located on the disk. 36 | # 37 | 38 | d1 = "LSI Logic PCI Fusion-MPT Miniport Driver",\symmpi.tag,\ 39 | 40 | 41 | [Defaults] 42 | 43 | # This section lists the default selection for each 'required' 44 | # hardware component. If a line is not present for a component, 45 | # the default defaults to the first item in the [] 46 | # section (see below). 47 | # 48 | # is one of computer, display, keyboard, mouse, scsi 49 | # is a unique string to be associated 50 | # with an option. 51 | 52 | scsi = SYMMPI_32 53 | 54 | 55 | [scsi] 56 | 57 | # This section lists the options available for a particular component. 58 | # 59 | # is the unique string for the option 60 | # is a text string, presented to the user in a menu 61 | # gives the name of the key to be created for the component in 62 | # HKEY_LOCAL_MACHINE\ControlSet001\Services 63 | 64 | SYMMPI_32 = "LSI Logic PCI Fusion-MPT Driver (XP 32-bit)",symmpi 65 | 66 | 67 | [HardwareIds.scsi.SYMMPI_32] 68 | 69 | id = "PCI\VEN_1000&DEV_0622", "symmpi" 70 | id = "PCI\VEN_1000&DEV_0624", "symmpi" 71 | id = "PCI\VEN_1000&DEV_0626", "symmpi" 72 | id = "PCI\VEN_1000&DEV_0628", "symmpi" 73 | id = "PCI\VEN_1000&DEV_0030", "symmpi" 74 | id = "PCI\VEN_1000&DEV_0032", "symmpi" 75 | id = "PCI\VEN_1000&DEV_0050", "symmpi" 76 | id = "PCI\VEN_1000&DEV_0054", "symmpi" 77 | id = "PCI\VEN_1000&DEV_0058", "symmpi" 78 | id = "PCI\VEN_1000&DEV_005E", "symmpi" 79 | id = "PCI\VEN_1000&DEV_0056", "symmpi" 80 | id = "PCI\VEN_1000&DEV_005A", "symmpi" 81 | id = "PCI\VEN_1000&DEV_0640", "symmpi" 82 | id = "PCI\VEN_1000&DEV_0646", "symmpi" 83 | id = "PCI\VEN_1000&DEV_0062", "symmpi" 84 | 85 | 86 | # This section lists the files that should be copied if the user 87 | # selects a particular component option. 88 | # 89 | # is one of driver, port, class, dll, hal, inf, or detect. 90 | # See below. 91 | # identifies where the file is to be copied from, and must 92 | # match en entry in the [Disks] section. 93 | # is the name of the file. This will be appended to the 94 | # directory specified for the disk in the [Disks] section to form the 95 | # full path of the file on the disk. 96 | 97 | [Files.scsi.SYMMPI_32] 98 | driver = d1,symmpi.sys,SYMMPI 99 | inf = d1,symmpi.inf 100 | inf = d1,lsipseud.inf 101 | catalog = d1,mpixp32.cat 102 | 103 | 104 | [Config.SYMMPI] 105 | 106 | # This section specifies values to be set in the registry for 107 | # particular component options. Required values in the services\\xxx 108 | # key are created automatically -- use this section to specify additional 109 | # keys to be created in services\\xxx and values in services\\xxx and 110 | # services\\xxx\\yyy. 111 | # 112 | # is relative to the services node for this device. 113 | # If it is empty, then it refers to the services node. 114 | # If specified, the key is created first. 115 | # specifies the value to be set within the key 116 | # is a string like REG_DWORD. See below. 117 | # specifies the actual value; its format depends on 118 | value = Parameters\PnpInterface,5,REG_DWORD,1 -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /tests/data/util/task/certificate/invalid.pem: -------------------------------------------------------------------------------- 1 | This is not a valid certificate. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Helpers for testing opsi. 7 | """ 8 | 9 | import os 10 | import shutil 11 | import tempfile 12 | from contextlib import contextmanager 13 | from unittest import mock 14 | 15 | from OPSI.Util.Path import cd 16 | 17 | 18 | @contextmanager 19 | def workInTemporaryDirectory(tempDir=None): 20 | """ 21 | Creates a temporary folder to work in. Deletes the folder afterwards. 22 | 23 | :param tempDir: use the given dir as temporary directory. Will not \ 24 | be deleted if given. 25 | """ 26 | temporary_folder = tempDir or tempfile.mkdtemp() 27 | with cd(temporary_folder): 28 | try: 29 | yield temporary_folder 30 | finally: 31 | if not tempDir: 32 | try: 33 | shutil.rmtree(temporary_folder) 34 | except OSError: 35 | pass 36 | 37 | 38 | @contextmanager 39 | def createTemporaryTestfile(original, tempDir=None): 40 | """Copy `original` to a temporary directory and \ 41 | yield the path to the new file. 42 | 43 | The temporary directory can be specified overridden with `tempDir`.""" 44 | 45 | with workInTemporaryDirectory(tempDir) as targetDir: 46 | shutil.copy(original, targetDir) 47 | 48 | filename = os.path.basename(original) 49 | 50 | yield os.path.join(targetDir, filename) 51 | 52 | 53 | def getLocalFQDN(): 54 | "Get the FQDN of the local machine." 55 | # Lazy imports to not hinder other tests. 56 | from OPSI.Types import forceHostId 57 | from OPSI.Util import getfqdn 58 | 59 | return forceHostId(getfqdn()) 60 | 61 | 62 | @contextmanager 63 | def patchAddress(fqdn="opsi.test.invalid", address="172.16.0.1"): 64 | """ 65 | Modify the results of socket so that expected addresses are returned. 66 | 67 | :param fqdn: The FQDN to use. Everything before the first '.' will serve\ 68 | as hostname. 69 | :param address: The IP address to use. 70 | """ 71 | hostname = fqdn.split(".")[0] 72 | 73 | def getfqdn(*_): 74 | return fqdn 75 | 76 | def gethostbyaddr(*_): 77 | return (fqdn, [hostname], [address]) 78 | 79 | with mock.patch("socket.getfqdn", getfqdn): 80 | with mock.patch("socket.gethostbyaddr", gethostbyaddr): 81 | yield 82 | 83 | 84 | @contextmanager 85 | def patchEnvironmentVariables(**environmentVariables): 86 | """ 87 | Patches to environment variables to be empty during the context. 88 | Anything supplied as keyword argument will be added to the environment. 89 | """ 90 | originalEnv = os.environ.copy() 91 | try: 92 | os.environ.clear() 93 | for key, value in environmentVariables.items(): 94 | os.environ[key] = value 95 | 96 | yield 97 | finally: 98 | os.environ = originalEnv 99 | 100 | 101 | @contextmanager 102 | def fakeGlobalConf(fqdn="opsi.test.invalid", dir=None): 103 | "Fake a global.conf and return the path to the file." 104 | 105 | with workInTemporaryDirectory(dir) as tempDir: 106 | configPath = os.path.join(tempDir, "global.conf") 107 | 108 | with open(configPath, "w", encoding="utf-8") as conf: 109 | conf.write("[global]\n") 110 | conf.write(f"hostname = {fqdn}\n") 111 | yield configPath 112 | -------------------------------------------------------------------------------- /tests/test_backend_backenddispatcher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing BackendDispatcher. 7 | """ 8 | 9 | import os 10 | 11 | import pytest 12 | 13 | from OPSI.Backend.BackendManager import BackendDispatcher 14 | from OPSI.Exceptions import BackendConfigurationError 15 | 16 | from .Backends.File import getFileBackend 17 | from .conftest import _backendBase 18 | 19 | 20 | @pytest.mark.parametrize( 21 | "kwargs", 22 | [ 23 | {}, 24 | {"dispatchConfigfile": ""}, 25 | {"dispatchConfigfile": "nope"}, 26 | {"dispatchConfig": ""}, 27 | {"dispatchConfig": [(".*", ("file",))]}, 28 | ], 29 | ) 30 | def testBackendCreationFailsIfConfigMissing(kwargs): 31 | with pytest.raises(BackendConfigurationError): 32 | BackendDispatcher(**kwargs) 33 | 34 | 35 | @pytest.mark.parametrize( 36 | "create_folder", [True, False], ids=["existing folder", "nonexisting folder"] 37 | ) 38 | def testLoadingDispatchConfigFailsIfBackendConfigWithoutConfigs(create_folder, tempDir): 39 | backendDir = os.path.join(tempDir, "backends") 40 | 41 | if create_folder: 42 | os.mkdir(backendDir) 43 | print("Created folder: {0}".format(backendDir)) 44 | 45 | with pytest.raises(BackendConfigurationError): 46 | BackendDispatcher( 47 | dispatchConfig=[[".*", ["file"]]], backendConfigDir=backendDir 48 | ) 49 | 50 | 51 | def testDispatchingMethodAndReceivingResults(dispatcher): 52 | assert [] == dispatcher.host_getObjects() 53 | 54 | 55 | def testLoadingDispatchConfig(dispatcher): 56 | assert "file" in dispatcher.dispatcher_getBackendNames() 57 | assert [(".*", ("file",))] == dispatcher.dispatcher_getConfig() 58 | 59 | 60 | @pytest.fixture 61 | def dispatcherBackend(tempDir): 62 | "A file backend for dispatching" 63 | with getFileBackend(tempDir) as backend: 64 | with _backendBase(backend): 65 | yield backend 66 | 67 | 68 | @pytest.fixture 69 | def dispatcher(dispatcherBackend, tempDir): 70 | "a BackendDispatcher running on a file backend." 71 | 72 | dispatchConfigPath = _patchDispatchConfigForFileBackend(tempDir) 73 | 74 | yield BackendDispatcher( 75 | dispatchConfigFile=dispatchConfigPath, 76 | backendConfigDir=os.path.join(tempDir, "etc", "opsi", "backends"), 77 | ) 78 | 79 | 80 | def _patchDispatchConfigForFileBackend(targetDirectory): 81 | configDir = os.path.join(targetDirectory, "etc", "opsi", "backendManager") 82 | dispatchConfigPath = os.path.join(configDir, "dispatch.conf") 83 | 84 | with open(dispatchConfigPath, "w") as dpconf: 85 | dpconf.write(""" 86 | .* : file 87 | """) 88 | 89 | return dispatchConfigPath 90 | -------------------------------------------------------------------------------- /tests/test_backend_extend_d_10_opsi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Tests for the dynamically loaded OPSI 3.x legacy methods. 7 | 8 | This tests what usually is found under 9 | ``/etc/opsi/backendManager/extend.de/10_opsi.conf``. 10 | """ 11 | 12 | from OPSI.Object import ( 13 | OpsiClient, 14 | LocalbootProduct, 15 | ProductOnClient, 16 | ProductDependency, 17 | OpsiDepotserver, 18 | ProductOnDepot, 19 | UnicodeConfig, 20 | ConfigState, 21 | ) 22 | 23 | import pytest 24 | 25 | 26 | @pytest.fixture 27 | def prefilledBackendManager(backendManager): 28 | fillBackend(backendManager) 29 | yield backendManager 30 | 31 | 32 | def fillBackend(backend): 33 | client, depot = createClientAndDepot(backend) 34 | 35 | firstProduct = LocalbootProduct("to_install", "1.0", "1.0") 36 | secondProduct = LocalbootProduct("already_installed", "1.0", "1.0") 37 | 38 | prodDependency = ProductDependency( 39 | productId=firstProduct.id, 40 | productVersion=firstProduct.productVersion, 41 | packageVersion=firstProduct.packageVersion, 42 | productAction="setup", 43 | requiredProductId=secondProduct.id, 44 | # requiredProductVersion=secondProduct.productVersion, 45 | # requiredPackageVersion=secondProduct.packageVersion, 46 | requiredAction="setup", 47 | requiredInstallationStatus="installed", 48 | requirementType="after", 49 | ) 50 | 51 | backend.product_createObjects([firstProduct, secondProduct]) 52 | backend.productDependency_createObjects([prodDependency]) 53 | 54 | poc = ProductOnClient( 55 | clientId=client.id, 56 | productId=firstProduct.id, 57 | productType=firstProduct.getType(), 58 | productVersion=firstProduct.productVersion, 59 | packageVersion=firstProduct.packageVersion, 60 | installationStatus="installed", 61 | actionResult="successful", 62 | ) 63 | 64 | backend.productOnClient_createObjects([poc]) 65 | 66 | firstProductOnDepot = ProductOnDepot( 67 | productId=firstProduct.id, 68 | productType=firstProduct.getType(), 69 | productVersion=firstProduct.productVersion, 70 | packageVersion=firstProduct.packageVersion, 71 | depotId=depot.getId(), 72 | locked=False, 73 | ) 74 | 75 | secondProductOnDepot = ProductOnDepot( 76 | productId=secondProduct.id, 77 | productType=secondProduct.getType(), 78 | productVersion=secondProduct.productVersion, 79 | packageVersion=secondProduct.packageVersion, 80 | depotId=depot.getId(), 81 | locked=False, 82 | ) 83 | 84 | backend.productOnDepot_createObjects([firstProductOnDepot, secondProductOnDepot]) 85 | 86 | 87 | def createClientAndDepot(backend): 88 | client = OpsiClient( 89 | id="backend-test-1.vmnat.local", description="Unittest Test client." 90 | ) 91 | 92 | depot = OpsiDepotserver( 93 | id="depotserver1.some.test", 94 | description="Test Depot", 95 | ) 96 | 97 | backend.host_createObjects([client, depot]) 98 | 99 | clientConfigDepotId = UnicodeConfig( 100 | id="clientconfig.depot.id", 101 | description="Depotserver to use", 102 | possibleValues=[], 103 | defaultValues=[depot.id], 104 | ) 105 | 106 | backend.config_createObjects(clientConfigDepotId) 107 | 108 | clientDepotMappingConfigState = ConfigState( 109 | configId=clientConfigDepotId.getId(), 110 | objectId=client.getId(), 111 | values=depot.getId(), 112 | ) 113 | 114 | backend.configState_createObjects(clientDepotMappingConfigState) 115 | 116 | return client, depot 117 | 118 | 119 | def testBackendDoesNotCreateProductsOnClientsOnItsOwn(prefilledBackendManager): 120 | pocs = prefilledBackendManager.productOnClient_getObjects() 121 | assert 1 == len( 122 | pocs 123 | ), "Expected to have only one ProductOnClient but got {n} instead: {0}".format( 124 | pocs, n=len(pocs) 125 | ) 126 | -------------------------------------------------------------------------------- /tests/test_backend_extend_d_10_wim.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | This tests what usually is found under 7 | ``/etc/opsi/backendManager/extend.de/10_wim.conf``. 8 | """ 9 | 10 | import os 11 | 12 | import pytest 13 | 14 | from OPSI.Object import NetbootProduct, ProductOnDepot, UnicodeProductProperty 15 | 16 | from .helpers import getLocalFQDN, mock, patchAddress, patchEnvironmentVariables 17 | from .test_hosts import getConfigServer 18 | from .test_util_wim import ( 19 | fakeWimPath, # required fixture # noqa: F401 20 | ) 21 | 22 | 23 | def test_update_wim(backendManager, fakeWimPath): # noqa: F811 24 | backend = backendManager 25 | localFqdn = getLocalFQDN() 26 | if "[mysql]" in os.environ["PYTEST_CURRENT_TEST"]: 27 | pytest.skip( 28 | "MySQL backend license check will not work with mocked os.path.exists" 29 | ) 30 | 31 | with patchAddress(fqdn=localFqdn): 32 | with patchEnvironmentVariables(OPSI_HOSTNAME=localFqdn): 33 | fill_backend(backend) 34 | 35 | with mock.patch("OPSI.Util.WIM.os.path.exists", lambda path: True): 36 | backend.updateWIMConfig("testwindows") 37 | 38 | imagename = backend.productProperty_getObjects( 39 | propertyId="imagename", productId="testwindows" 40 | ) 41 | imagename = imagename[0] 42 | 43 | possibleImageNames = set( 44 | [ 45 | "Windows 7 HOMEBASICN", 46 | "Windows 7 HOMEPREMIUMN", 47 | "Windows 7 PROFESSIONALN", 48 | "Windows 7 STARTERN", 49 | "Windows 7 ULTIMATEN", 50 | ] 51 | ) 52 | assert possibleImageNames == set(imagename.possibleValues) 53 | assert imagename.defaultValues[0] in imagename.possibleValues 54 | 55 | language = backend.productProperty_getObjects( 56 | propertyId="system_language", productId="testwindows" 57 | ) 58 | language = language[0] 59 | assert ["de-DE"] == language.defaultValues 60 | assert ["de-DE"] == language.possibleValues 61 | 62 | 63 | @pytest.mark.parametrize("objectId", ["", None]) 64 | def test_updating_wim_fails_with_invalid_object_id(backendManager, objectId): 65 | with pytest.raises(ValueError): 66 | backendManager.updateWIMConfig(objectId) 67 | 68 | 69 | def test_updating_wim_fails_with_invalid_product_id(backendManager): 70 | with pytest.raises(OSError): 71 | backendManager.updateWIMConfigFromPath("", "") 72 | 73 | 74 | def fill_backend(backend): 75 | configServer = getConfigServer() 76 | backend.host_insertObject(configServer) 77 | 78 | product = NetbootProduct(id="testWindows", productVersion=1, packageVersion=1) 79 | backend.product_insertObject(product) 80 | 81 | productOnDepot = ProductOnDepot( 82 | productId=product.id, 83 | productType=product.getType(), 84 | productVersion=product.productVersion, 85 | packageVersion=product.packageVersion, 86 | depotId=configServer.id, 87 | locked=False, 88 | ) 89 | backend.productOnDepot_insertObject(productOnDepot) 90 | 91 | imagenameProductProperty = UnicodeProductProperty( 92 | productId=product.id, 93 | productVersion=product.productVersion, 94 | packageVersion=product.packageVersion, 95 | propertyId="imagename", 96 | possibleValues=["NOT YOUR IMAGE", "NO NO NO"], 97 | defaultValues=["NOT YOUR IMAGE"], 98 | editable=True, 99 | multiValue=False, 100 | ) 101 | systemLanguageProductProperty = UnicodeProductProperty( 102 | productId=product.id, 103 | productVersion=product.productVersion, 104 | packageVersion=product.packageVersion, 105 | propertyId="system_language", 106 | possibleValues=["lol_NOPE"], 107 | defaultValues=["lol_NOPE", "rofl_MAO"], 108 | editable=True, 109 | multiValue=False, 110 | ) 111 | winpeUilanguageProductProperty = UnicodeProductProperty( 112 | productId=product.id, 113 | productVersion=product.productVersion, 114 | packageVersion=product.packageVersion, 115 | propertyId="winpe_uilanguage", 116 | possibleValues=["lel"], 117 | defaultValues=["lel", "topkek"], 118 | editable=True, 119 | multiValue=False, 120 | ) 121 | winpeUilanguageFallbackProductProperty = UnicodeProductProperty( 122 | productId=product.id, 123 | productVersion=product.productVersion, 124 | packageVersion=product.packageVersion, 125 | propertyId="winpe_uilanguage_fallback", 126 | possibleValues=["lachkadse"], 127 | defaultValues=["lachkadse", "freuvieh"], 128 | editable=True, 129 | multiValue=False, 130 | ) 131 | backend.productProperty_insertObject(imagenameProductProperty) 132 | backend.productProperty_insertObject(systemLanguageProductProperty) 133 | backend.productProperty_insertObject(winpeUilanguageProductProperty) 134 | backend.productProperty_insertObject(winpeUilanguageFallbackProductProperty) 135 | -------------------------------------------------------------------------------- /tests/test_backend_extend_d_70_wan.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) uib GmbH 5 | # License: AGPL-3.0 6 | """ 7 | Tests for easy configuration of WAN clients. 8 | 9 | This tests what usually is found under 10 | ``/etc/opsi/backendManager/extend.de/70_wan.conf``. 11 | 12 | .. versionadded:: 4.0.6.3 13 | """ 14 | 15 | from __future__ import print_function 16 | 17 | import pytest 18 | 19 | from OPSI.Object import OpsiClient 20 | from OPSI.Util.Task.ConfigureBackend.ConfigurationData import createWANconfigs 21 | 22 | 23 | @pytest.fixture 24 | def backendWithWANConfigs(backendManager): 25 | createWANconfigs(backendManager) 26 | yield backendManager 27 | 28 | 29 | def clientHasWANEnabled(backend, clientId): 30 | configsToCheck = set( 31 | [ 32 | "opsiclientd.event_gui_startup.active", 33 | "opsiclientd.event_gui_startup{user_logged_in}.active", 34 | "opsiclientd.event_net_connection.active", 35 | "opsiclientd.event_timer.active", 36 | ] 37 | ) 38 | 39 | for configState in backend.configState_getObjects(objectId=clientId): 40 | if configState.configId == "opsiclientd.event_gui_startup.active": 41 | if configState.values[0]: 42 | return False 43 | configsToCheck.remove("opsiclientd.event_gui_startup.active") 44 | elif ( 45 | configState.configId 46 | == "opsiclientd.event_gui_startup{user_logged_in}.active" 47 | ): 48 | if configState.values[0]: 49 | return False 50 | configsToCheck.remove( 51 | "opsiclientd.event_gui_startup{user_logged_in}.active" 52 | ) 53 | elif configState.configId == "opsiclientd.event_net_connection.active": 54 | if not configState.values[0]: 55 | return False 56 | configsToCheck.remove("opsiclientd.event_net_connection.active") 57 | elif configState.configId == "opsiclientd.event_timer.active": 58 | if not configState.values[0]: 59 | return False 60 | configsToCheck.remove("opsiclientd.event_timer.active") 61 | 62 | if configsToCheck: 63 | print("The following configs were not set: {0}".format(configsToCheck)) 64 | return False 65 | 66 | return True 67 | 68 | 69 | def testEnablingSettingForOneHost(backendWithWANConfigs): 70 | backend = backendWithWANConfigs 71 | clientId = "testclient.test.invalid" 72 | backend.host_createObjects(OpsiClient(id=clientId)) 73 | 74 | backend.changeWANConfig(True, clientId) 75 | assert clientHasWANEnabled(backend, clientId) 76 | 77 | backend.changeWANConfig(False, clientId) 78 | assert not clientHasWANEnabled(backend, clientId) 79 | 80 | 81 | def testEnablingSettingForMultipleHosts(backendWithWANConfigs): 82 | backend = backendWithWANConfigs 83 | 84 | clientIds = ["testclient{0}.test.invalid".format(num) for num in range(10)] 85 | backend.host_createObjects([OpsiClient(id=clientId) for clientId in clientIds]) 86 | 87 | backend.changeWANConfig(True, clientIds) 88 | 89 | for clientId in clientIds: 90 | assert clientHasWANEnabled(backend, clientId) 91 | 92 | 93 | def testNotFailingOnEmptyList(backendWithWANConfigs): 94 | backendWithWANConfigs.changeWANConfig(True, []) 95 | 96 | 97 | def testNotChangingUnreferencedClient(backendWithWANConfigs): 98 | backend = backendWithWANConfigs 99 | 100 | clientIds = ["testclient{0}.test.invalid".format(num) for num in range(10)] 101 | singleClient = "testclient99.test.invalid" 102 | backend.host_createObjects([OpsiClient(id=clientId) for clientId in clientIds]) 103 | backend.host_createObjects([OpsiClient(id=singleClient)]) 104 | 105 | backend.changeWANConfig(True, clientIds) 106 | backend.changeWANConfig(True, []) 107 | 108 | for clientId in clientIds: 109 | assert clientHasWANEnabled(backend, clientId) 110 | 111 | assert not clientHasWANEnabled(backend, singleClient) 112 | 113 | 114 | @pytest.mark.parametrize( 115 | "value, expected", 116 | [ 117 | ("on", True), 118 | ("1", True), 119 | ("true", True), 120 | ("off", False), 121 | ("false", False), 122 | ("0", False), 123 | ], 124 | ) 125 | def testUsingNonBooleanParameters(backendWithWANConfigs, value, expected): 126 | backend = backendWithWANConfigs 127 | 128 | client = OpsiClient(id="testclient101.test.invalid") 129 | backend.host_createObjects([client]) 130 | 131 | backend.changeWANConfig(value, client.id) 132 | assert clientHasWANEnabled(backend, client.id) == expected 133 | -------------------------------------------------------------------------------- /tests/test_backend_file.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing the opsi file backend. 7 | """ 8 | 9 | import pytest 10 | 11 | from OPSI.Backend.File import FileBackend 12 | from OPSI.Exceptions import BackendConfigurationError 13 | 14 | from .Backends.File import getFileBackend 15 | 16 | 17 | def testGetRawDataFailsOnFileBackendBecauseMissingQuerySupport(): 18 | with getFileBackend() as backend: 19 | with pytest.raises(BackendConfigurationError): 20 | backend.getRawData("SELECT * FROM BAR;") 21 | 22 | 23 | def testGetDataFailsOnFileBackendBecauseMissingQuerySupport(): 24 | with getFileBackend() as backend: 25 | with pytest.raises(BackendConfigurationError): 26 | backend.getData("SELECT * FROM BAR;") 27 | 28 | 29 | @pytest.mark.parametrize( 30 | "filename", 31 | [ 32 | "exampleexam_e.-ex_1234.12-1234.12.localboot", 33 | ], 34 | ) 35 | def testProductFilenamePattern(filename): 36 | assert FileBackend.PRODUCT_FILENAME_REGEX.search(filename) is not None 37 | -------------------------------------------------------------------------------- /tests/test_backend_hostcontrol.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing the Host Control backend. 7 | """ 8 | 9 | from ipaddress import IPv4Address, IPv4Network 10 | 11 | import pytest 12 | from opsicommon.objects import OpsiClient 13 | 14 | from OPSI.Backend.HostControl import HostControlBackend 15 | from OPSI.Exceptions import BackendMissingDataError 16 | 17 | from .test_hosts import getClients 18 | 19 | 20 | @pytest.fixture 21 | def host_control_backend(extendedConfigDataBackend): 22 | yield HostControlBackend(extendedConfigDataBackend) 23 | 24 | 25 | @pytest.mark.parametrize( 26 | "config, expected_result", 27 | ( 28 | ( 29 | ["255.255.255.255", "192.168.10.255"], 30 | { 31 | IPv4Network("0.0.0.0/0"): { 32 | IPv4Address("255.255.255.255"): (7, 9, 12287), 33 | IPv4Address("192.168.10.255"): (7, 9, 12287), 34 | } 35 | }, 36 | ), 37 | ( 38 | ["255.255.255.255"], 39 | {IPv4Network("0.0.0.0/0"): {IPv4Address("255.255.255.255"): (7, 9, 12287)}}, 40 | ), 41 | ( 42 | {"255.255.255.255": [9, 12287], "10.10.255.255": [12287]}, 43 | { 44 | IPv4Network("0.0.0.0/0"): { 45 | IPv4Address("255.255.255.255"): (9, 12287), 46 | IPv4Address("10.10.255.255"): (12287,), 47 | } 48 | }, 49 | ), 50 | ( 51 | { 52 | "0.0.0.0/0": {"255.255.255.255": [9, 12287]}, 53 | "10.10.0.0/16": {"10.10.1.255": [9, 12287], "10.10.2.255": [12287]}, 54 | }, 55 | { 56 | IPv4Network("0.0.0.0/0"): { 57 | IPv4Address("255.255.255.255"): (9, 12287), 58 | }, 59 | IPv4Network("10.10.0.0/16"): { 60 | IPv4Address("10.10.1.255"): (9, 12287), 61 | IPv4Address("10.10.2.255"): (12287,), 62 | }, 63 | }, 64 | ), 65 | ), 66 | ) 67 | def test_set_broadcast_addresses(host_control_backend, config, expected_result): 68 | host_control_backend._set_broadcast_addresses(config) 69 | assert host_control_backend._broadcastAddresses == expected_result 70 | 71 | 72 | @pytest.mark.parametrize( 73 | "config, ip_address, expected_result", 74 | ( 75 | ( 76 | ["255.255.255.255", "192.168.10.255"], 77 | None, 78 | [("255.255.255.255", (7, 9, 12287)), ("192.168.10.255", (7, 9, 12287))], 79 | ), 80 | ( 81 | { 82 | "0.0.0.0/0": {"255.255.255.255": [9, 12287]}, 83 | "10.10.0.0/16": {"10.10.1.255": [9, 12287], "10.10.2.255": [12287]}, 84 | }, 85 | None, 86 | [ 87 | ("255.255.255.255", (9, 12287)), 88 | ("10.10.1.255", (9, 12287)), 89 | ("10.10.2.255", (12287,)), 90 | ], 91 | ), 92 | ( 93 | { 94 | "192.0.0.0/8": {"255.255.255.255": [9, 12287]}, 95 | "10.10.0.0/16": {"10.10.1.255": [9, 12287], "10.10.2.255": [12287]}, 96 | }, 97 | "10.1.1.1", 98 | [ 99 | ("255.255.255.255", (9, 12287)), 100 | ("10.10.1.255", (9, 12287)), 101 | ("10.10.2.255", (12287,)), 102 | ], 103 | ), 104 | ( 105 | { 106 | "0.0.0.0/0": {"255.255.255.255": [9, 12287]}, 107 | "10.10.0.0/16": {"10.10.1.255": [9, 12287], "10.10.2.255": [12287]}, 108 | }, 109 | "10.10.1.1", 110 | [("10.10.1.255", (9, 12287)), ("10.10.2.255", (12287,))], 111 | ), 112 | ( 113 | { 114 | "192.168.0.0/16": {"255.255.255.255": [9, 12287]}, 115 | "10.10.1.0/24": {"10.10.1.255": [9, 12287]}, 116 | "10.10.2.0/24": {"10.10.2.255": [12287]}, 117 | }, 118 | "10.10.2.1", 119 | [("10.10.2.255", (12287,))], 120 | ), 121 | ), 122 | ) 123 | def test_get_broadcast_addresses_for_host( 124 | host_control_backend, config, ip_address, expected_result 125 | ): 126 | host_control_backend._set_broadcast_addresses(config) 127 | host = OpsiClient(id="test.opsi.org", ipAddress=ip_address) 128 | assert ( 129 | list(host_control_backend._get_broadcast_addresses_for_host(host)) 130 | == expected_result 131 | ) 132 | 133 | 134 | def test_calling_start_and_stop_method(host_control_backend): 135 | """ 136 | Test if calling the methods works. 137 | 138 | This test does not check if WOL on these clients work nor that 139 | they do exist. 140 | """ 141 | clients = getClients() 142 | host_control_backend.host_createObjects(clients) 143 | host_control_backend._hostRpcTimeout = 1 # for faster finishing of the test 144 | host_control_backend.hostControl_start(["client1.test.invalid"]) 145 | host_control_backend.hostControl_shutdown(["client1.test.invalid"]) 146 | 147 | 148 | def test_host_control_reachable_without_hosts(host_control_backend): 149 | with pytest.raises(BackendMissingDataError): 150 | host_control_backend.hostControl_reachable() 151 | -------------------------------------------------------------------------------- /tests/test_backend_jsonrpc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing jsonrpc backend functionality. 7 | """ 8 | 9 | import json 10 | from pathlib import Path 11 | from typing import Any 12 | 13 | import pytest 14 | from opsicommon.exceptions import OpsiServiceConnectionError 15 | from opsicommon.testing.helpers import http_test_server 16 | 17 | from OPSI.Backend.JSONRPC import JSONRPCBackend 18 | 19 | 20 | def test_jsonrpc_backend(tmp_path: Path) -> None: 21 | log_file = tmp_path / "request.log" 22 | interface: list[dict[str, Any]] = [ 23 | { 24 | "name": "test_method", 25 | "params": ["arg1", "*arg2", "**arg3"], 26 | "args": ["arg1", "arg2"], 27 | "varargs": None, 28 | "keywords": "arg4", 29 | "defaults": ["default2"], 30 | "deprecated": False, 31 | "alternative_method": None, 32 | "doc": None, 33 | "annotations": {}, 34 | }, 35 | { 36 | "name": "backend_getInterface", 37 | "params": [], 38 | "args": ["self"], 39 | "varargs": None, 40 | "keywords": None, 41 | "defaults": None, 42 | "deprecated": False, 43 | "alternative_method": None, 44 | "doc": None, 45 | "annotations": {}, 46 | }, 47 | { 48 | "name": "backend_exit", 49 | "params": [], 50 | "args": ["self"], 51 | "varargs": None, 52 | "keywords": None, 53 | "defaults": None, 54 | "deprecated": False, 55 | "alternative_method": None, 56 | "doc": None, 57 | "annotations": {}, 58 | }, 59 | ] 60 | with http_test_server( 61 | generate_cert=True, 62 | log_file=log_file, 63 | response_headers={"server": "opsiconfd 4.3.0.0 (uvicorn)"}, 64 | ) as server: 65 | server.response_body = json.dumps( 66 | {"jsonrpc": "2.0", "result": interface} 67 | ).encode("utf-8") 68 | server.response_headers["Content-Type"] = "application/json" 69 | backend = JSONRPCBackend(address=f"https://localhost:{server.port}") 70 | backend.test_method("arg1") 71 | 72 | with pytest.raises(OpsiServiceConnectionError): 73 | backend = JSONRPCBackend(address=f"https://localhost:{server.port+1}") 74 | -------------------------------------------------------------------------------- /tests/test_backend_methods.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing unbound methods for the backends. 7 | """ 8 | 9 | from OPSI.Backend.Base.Extended import get_function_signature_and_args 10 | 11 | 12 | def test_getting_signature_for_method_without_arguments(): 13 | def foo(): 14 | pass 15 | 16 | sig, args = get_function_signature_and_args(foo) 17 | 18 | assert sig == "()" 19 | assert not args 20 | 21 | 22 | def test_getting_signature_for_method_with_one_positional_argument(): 23 | def foo(bar): 24 | pass 25 | 26 | sig, args = get_function_signature_and_args(foo) 27 | 28 | assert sig == "(bar)" 29 | assert args == "bar=bar" 30 | 31 | 32 | def test_getting_signature_for_method_with_multiple_positional_arguments(): 33 | def foo(bar, baz): 34 | pass 35 | 36 | sig, args = get_function_signature_and_args(foo) 37 | 38 | assert sig == "(bar, baz)" 39 | assert args == "bar=bar, baz=baz" 40 | 41 | 42 | def test_getting_signature_for_method_with_keyword_argument_only(): 43 | def foo(bar=None): 44 | pass 45 | 46 | sig, args = get_function_signature_and_args(foo) 47 | 48 | assert "(bar=None)" == sig 49 | assert "bar=bar" == args 50 | 51 | 52 | def test_getting_signature_for_method_with_multiple_keyword_arguments_only(): 53 | def foo(bar=None, baz=None): 54 | pass 55 | 56 | sig, args = get_function_signature_and_args(foo) 57 | 58 | assert sig == "(bar=None, baz=None)" 59 | assert args == "bar=bar, baz=baz" 60 | 61 | 62 | def test_getting_signature_for_method_with_mixed_arguments(): 63 | def foo(bar, baz=None): 64 | pass 65 | 66 | sig, args = get_function_signature_and_args(foo) 67 | 68 | assert sig == "(bar, baz=None)" 69 | assert args == "bar=bar, baz=baz" 70 | 71 | 72 | def test_self_as_first_argument_is_ignored(): 73 | def foo(self, bar=None): 74 | pass 75 | 76 | sig, args = get_function_signature_and_args(foo) 77 | 78 | assert sig == "(bar=None)" 79 | assert args == "bar=bar" 80 | 81 | 82 | def test_argument_with_string_default(): 83 | def foo(bar="baz"): 84 | pass 85 | 86 | sig, args = get_function_signature_and_args(foo) 87 | 88 | assert sig == "(bar='baz')" 89 | assert args == "bar=bar" 90 | 91 | 92 | def test_argument_with_variable_argument_count(): 93 | def foo(*bar): 94 | pass 95 | 96 | sig, args = get_function_signature_and_args(foo) 97 | 98 | assert sig == "(*bar)" 99 | assert args == "*bar" 100 | 101 | 102 | def test_argument_with_positional_argument_and_variable_argument_count(): 103 | def foo(bar, *baz): 104 | pass 105 | 106 | sig, args = get_function_signature_and_args(foo) 107 | 108 | assert sig == "(bar, *baz)" 109 | assert args == "bar=bar, *baz" 110 | 111 | 112 | def test_variable_keyword_arguments(): 113 | def foo(**bar): 114 | pass 115 | 116 | sig, args = get_function_signature_and_args(foo) 117 | 118 | assert sig == "(**bar)" 119 | assert args == "**bar" 120 | 121 | 122 | def test_method_with_all_types_of_arguments(): 123 | def foo(ironman, blackWidow=True, *hulk, **deadpool): 124 | pass 125 | 126 | sig, args = get_function_signature_and_args(foo) 127 | 128 | assert sig == "(ironman, blackWidow=True, *hulk, **deadpool)" 129 | assert args == "ironman=ironman, blackWidow=blackWidow, *hulk, **deadpool" 130 | 131 | 132 | def test_method_with_all_types_of_arguments_and_annotations(): 133 | def foo(self, ironman, blackWidow: bool = True, *hulk, **deadpool) -> int: 134 | return 1 135 | 136 | sig, args = get_function_signature_and_args(foo) 137 | 138 | assert sig == "(ironman, blackWidow: bool = True, *hulk, **deadpool) -> int" 139 | assert args == "ironman=ironman, blackWidow=blackWidow, *hulk, **deadpool" 140 | -------------------------------------------------------------------------------- /tests/test_backend_modificationtracker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing the modification tracking. 7 | 8 | Based on work of Christian Kampka. 9 | """ 10 | 11 | from OPSI.Backend.Backend import ModificationTrackingBackend 12 | from OPSI.Object import OpsiClient 13 | 14 | from .Backends.SQLite import getSQLiteBackend, getSQLiteModificationTracker 15 | from .Backends.MySQL import getMySQLBackend, getMySQLModificationTracker 16 | 17 | import pytest 18 | 19 | 20 | @pytest.fixture( 21 | params=[ 22 | (getSQLiteBackend, getSQLiteModificationTracker), 23 | (getMySQLBackend, getMySQLModificationTracker), 24 | ], 25 | ids=["sqlite", "mysql"], 26 | ) 27 | def backendAndTracker(request): 28 | backendFunc, trackerFunc = request.param 29 | with backendFunc() as basebackend: 30 | basebackend.backend_createBase() 31 | 32 | backend = ModificationTrackingBackend(basebackend) 33 | 34 | with trackerFunc() as tracker: 35 | backend.addBackendChangeListener(tracker) 36 | 37 | yield backend, tracker 38 | 39 | # When reusing a database there may be leftover modifications! 40 | tracker.clearModifications() 41 | 42 | backend.backend_deleteBase() 43 | 44 | 45 | def testTrackingOfInsertObject(backendAndTracker): 46 | backend, tracker = backendAndTracker 47 | 48 | host = OpsiClient(id="client1.test.invalid") 49 | backend.host_insertObject(host) 50 | 51 | modifications = tracker.getModifications() 52 | assert 1 == len(modifications) 53 | mod = modifications[0] 54 | assert mod["objectClass"] == host.__class__.__name__ 55 | assert mod["command"] == "insert" 56 | assert mod["ident"] == host.getIdent() 57 | 58 | 59 | def testTrackingOfUpdatingObject(backendAndTracker): 60 | backend, tracker = backendAndTracker 61 | 62 | host = OpsiClient(id="client1.test.invalid") 63 | 64 | backend.host_insertObject(host) 65 | tracker.clearModifications() 66 | backend.host_updateObject(host) 67 | 68 | modifications = tracker.getModifications() 69 | assert 1 == len(modifications) 70 | mod = modifications[0] 71 | assert mod["objectClass"] == host.__class__.__name__ 72 | assert mod["command"] == "update" 73 | assert mod["ident"] == host.getIdent() 74 | 75 | 76 | @pytest.mark.requires_license_file 77 | def testTrackingOfDeletingObject(backendAndTracker): 78 | backend, tracker = backendAndTracker 79 | 80 | host = OpsiClient(id="client1.test.invalid") 81 | 82 | backend.host_insertObject(host) 83 | tracker.clearModifications() 84 | backend.host_deleteObjects(host) 85 | 86 | modifications = tracker.getModifications() 87 | 88 | assert 1 == len(modifications) 89 | modification = modifications[0] 90 | 91 | assert modification["objectClass"] == host.__class__.__name__ 92 | assert modification["command"] == "delete" 93 | assert modification["ident"] == host.getIdent() 94 | -------------------------------------------------------------------------------- /tests/test_backend_multithreading.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing backend multithreading behaviour. 7 | """ 8 | 9 | import threading 10 | import time 11 | 12 | import pytest 13 | 14 | from OPSI.Backend.Backend import ExtendedConfigDataBackend 15 | from .test_groups import fillBackendWithObjectToGroups 16 | 17 | 18 | @pytest.mark.parametrize("numberOfThreads", [50]) 19 | def testMultiThreadingBackend(multithreadingBackend, numberOfThreads): 20 | backend = ExtendedConfigDataBackend(multithreadingBackend) 21 | 22 | o2g, _, clients = fillBackendWithObjectToGroups(backend) 23 | 24 | print("====================START=============================") 25 | 26 | class MultiThreadTester(threading.Thread): 27 | def __init__(self, backend, clients, objectToGroups): 28 | threading.Thread.__init__(self) 29 | 30 | self.error = None 31 | 32 | self.backend = backend 33 | self.clients = clients 34 | self.objectToGroups = objectToGroups 35 | 36 | def run(self): 37 | self.client1 = clients[0] 38 | self.client2 = clients[1] 39 | self.objectToGroup1 = o2g[0] 40 | self.objectToGroup2 = o2g[1] 41 | 42 | try: 43 | print("Thread %s started" % self) 44 | time.sleep(1) 45 | self.backend.host_getObjects() 46 | self.backend.host_deleteObjects(self.client1) 47 | 48 | self.backend.host_getObjects() 49 | self.backend.host_deleteObjects(self.client2) 50 | 51 | self.backend.host_createObjects(self.client2) 52 | self.backend.host_createObjects(self.client1) 53 | self.backend.objectToGroup_createObjects(self.objectToGroup1) 54 | self.backend.objectToGroup_createObjects(self.objectToGroup2) 55 | 56 | self.backend.host_getObjects() 57 | self.backend.host_createObjects(self.client1) 58 | self.backend.host_deleteObjects(self.client2) 59 | self.backend.host_createObjects(self.client1) 60 | self.backend.host_getObjects() 61 | except Exception as err: 62 | if "duplicate entry" in str(err).lower(): 63 | # Allow duplicate entry error 64 | pass 65 | else: 66 | self.error = err 67 | finally: 68 | print("Thread %s done" % self) 69 | 70 | mtts = [MultiThreadTester(backend, clients, o2g) for i in range(numberOfThreads)] 71 | for mtt in mtts: 72 | mtt.start() 73 | 74 | for mtt in mtts: 75 | mtt.join() 76 | 77 | client1 = clients[0] 78 | backend.host_createObjects(client1) 79 | 80 | while mtts: 81 | mtt = mtts.pop(0) 82 | if not mtt.is_alive(): 83 | assert not mtt.error, f"Multithreading test failed: Exit Code {mtt.error}" 84 | else: 85 | mtts.append(mtt) 86 | -------------------------------------------------------------------------------- /tests/test_backend_sqlite.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing the opsi SQLite backend. 7 | """ 8 | 9 | import pytest 10 | 11 | 12 | def testInitialisationOfSQLiteBackendWithoutParametersDoesNotFail(): 13 | sqlModule = pytest.importorskip("OPSI.Backend.SQLite") 14 | SQLiteBackend = sqlModule.SQLiteBackend 15 | 16 | backend = SQLiteBackend() 17 | backend.backend_createBase() 18 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing opsi config module. 7 | """ 8 | 9 | from OPSI.Config import ( 10 | DEFAULT_DEPOT_USER, 11 | FILE_ADMIN_GROUP, 12 | OPSI_ADMIN_GROUP, 13 | OPSI_GLOBAL_CONF, 14 | OPSICONFD_USER, 15 | ) 16 | 17 | import pytest 18 | 19 | 20 | @pytest.mark.parametrize( 21 | "value", 22 | [ 23 | FILE_ADMIN_GROUP, 24 | OPSI_ADMIN_GROUP, 25 | DEFAULT_DEPOT_USER, 26 | OPSI_GLOBAL_CONF, 27 | OPSICONFD_USER, 28 | ], 29 | ) 30 | def testValueIsSet(value): 31 | assert value is not None 32 | assert value 33 | -------------------------------------------------------------------------------- /tests/test_hardware_audit.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing hardware audit behaviour. 7 | """ 8 | 9 | import pytest 10 | from OPSI.Object import AuditHardwareOnHost, OpsiClient 11 | 12 | 13 | def testHardwareAuditAcceptingHugeMemoryClockSpeeds(hardwareAuditBackendWithHistory): 14 | """ 15 | Testing that the backend accepts a memory speed larger than the size 16 | of an signed int in MySQL. 17 | 18 | Datatype sizes are: 19 | * signed INT from -2147483648 to 2147483647 20 | * signed BIGINT from -9223372036854775808 to 9223372036854775807 21 | """ 22 | backend = hardwareAuditBackendWithHistory 23 | 24 | client = OpsiClient(id="foo.bar.invalid") 25 | backend.host_insertObject(client) 26 | 27 | backend.auditHardwareOnHost_setObsolete([client.id]) 28 | backend.auditHardwareOnHost_updateObjects( 29 | [ 30 | { 31 | "hostId": client.id, 32 | "vendor": "Micron", 33 | "description": "Physikalischer Speicher", 34 | "tag": "Physical Memory 0", 35 | "speed": 2400000000, 36 | "hardwareClass": "MEMORY_MODULE", 37 | "formFactor": "SODIMM", 38 | "capacity": "8589934592", 39 | "name": "DIMM 1", 40 | "serialNumber": "15E64109", 41 | "memoryType": "Unknown", 42 | "type": "AuditHardwareOnHost", 43 | "deviceLocator": "DIMM 1", 44 | "dataWidth": 64, 45 | }, 46 | ] 47 | ) 48 | 49 | 50 | @pytest.mark.parametrize("objects", [1, 5, 50]) 51 | def testHardwareAuditObsoletingOldHardware(hardwareAuditBackendWithHistory, objects): 52 | backend = hardwareAuditBackendWithHistory 53 | 54 | client = OpsiClient(id="foo.bar.invalid") 55 | backend.host_insertObject(client) 56 | 57 | for _ in range(objects): 58 | ahoh = { 59 | "hostId": client.id, 60 | "vendor": "Micron", 61 | "description": "Physikalischer Speicher", 62 | "tag": "Physical Memory 0", 63 | "speed": 2400000000, 64 | "hardwareClass": "MEMORY_MODULE", 65 | "formFactor": "SODIMM", 66 | "capacity": "8589934592", 67 | "name": "DIMM 1", 68 | "serialNumber": "15E64109", 69 | "memoryType": "Unknown", 70 | "model": None, 71 | "type": "AuditHardwareOnHost", 72 | "deviceLocator": "DIMM 1", 73 | "dataWidth": 64, 74 | "state": 1, 75 | } 76 | 77 | backend.auditHardwareOnHost_createObjects([ahoh]) 78 | 79 | backend.auditHardwareOnHost_setObsolete([client.id]) 80 | 81 | 82 | @pytest.mark.parametrize("objects", [1, 5, 50]) 83 | def testHardwareAuditObsoletingAllObjects(hardwareAuditBackendWithHistory, objects): 84 | backend = hardwareAuditBackendWithHistory 85 | 86 | client = OpsiClient(id="foo.bar.invalid") 87 | backend.host_insertObject(client) 88 | 89 | for _ in range(objects): 90 | ahoh = { 91 | "hostId": client.id, 92 | "vendor": "Micron", 93 | "description": "Physikalischer Speicher", 94 | "tag": "Physical Memory 0", 95 | "speed": 2400000000, 96 | "hardwareClass": "MEMORY_MODULE", 97 | "formFactor": "SODIMM", 98 | "capacity": "8589934592", 99 | "name": "DIMM 1", 100 | "serialNumber": "15E64109", 101 | "memoryType": "Unknown", 102 | "model": None, 103 | "type": "AuditHardwareOnHost", 104 | "deviceLocator": "DIMM 1", 105 | "dataWidth": 64, 106 | "state": 1, 107 | } 108 | 109 | backend.auditHardwareOnHost_createObjects([ahoh]) 110 | 111 | backend.auditHardwareOnHost_setObsolete(None) 112 | 113 | 114 | def testUpdatingAuditHardware(hardwareAuditBackendWithHistory): 115 | backend = hardwareAuditBackendWithHistory 116 | 117 | client = OpsiClient(id="foo.bar.invalid") 118 | backend.host_insertObject(client) 119 | 120 | ahoh = { 121 | "hostId": client.id, 122 | "vendor": "Micron", 123 | "description": "Physikalischer Speicher", 124 | "tag": "Physical Memory 0", 125 | "speed": 2400000000, 126 | "hardwareClass": "MEMORY_MODULE", 127 | "formFactor": "SODIMM", 128 | "capacity": "8589934592", 129 | "name": "DIMM 1", 130 | "serialNumber": "15E64109", 131 | "memoryType": "Unknown", 132 | "type": "AuditHardwareOnHost", 133 | "deviceLocator": "DIMM 1", 134 | "dataWidth": 64, 135 | } 136 | 137 | backend.auditHardwareOnHost_createObjects([ahoh]) 138 | backend.auditHardwareOnHost_updateObjects([ahoh]) 139 | 140 | 141 | def testAccepting10GBNetworkInterfaces(hardwareAuditBackendWithHistory): 142 | backend = hardwareAuditBackendWithHistory 143 | 144 | nic = { 145 | "vendorId": "15AD", 146 | "macAddress": "00:50:56:af:fe:af", 147 | "hardwareClass": "NETWORK_CONTROLLER", 148 | "subsystemVendorId": "15AD", 149 | "type": "AuditHardwareOnHost", 150 | "revision": "01", 151 | "hostId": "machine.test.invalid", 152 | "vendor": "VMware", 153 | "description": "Ethernet interface", 154 | "subsystemDeviceId": "07B0", 155 | "deviceId": "07B0", 156 | "autoSense": "off", 157 | "netConnectionStatus": "yes", 158 | "maxSpeed": 10000000000, 159 | "name": "VMXNET3 Ethernet Controller", 160 | "serialNumber": "00:50:56:af:fe:af", 161 | "model": "VMXNET3 Ethernet Controller", 162 | "ipAddress": "192.168.123.45", 163 | "adapterType": "twisted pair", 164 | } 165 | auditHardware = AuditHardwareOnHost.fromHash(nic) 166 | 167 | backend.auditHardwareOnHost_insertObject(auditHardware) 168 | -------------------------------------------------------------------------------- /tests/test_system_posix_distribution.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing Distribution functionality from OPSI.System.Posix 7 | """ 8 | 9 | from OPSI.System.Posix import Distribution 10 | 11 | import pytest 12 | 13 | 14 | # The first tuple is retrieved by running platform.linux_distribution() 15 | # or distro.linux_distribution() on the corresponding version. 16 | DISTRI_INFOS = [ 17 | (("debian", "8.11", ""), (8, 11)), 18 | (("debian", "10.0", ""), (10, 0)), 19 | # TODO: add CentOS 7 20 | (("Red Hat Enterprise Linux Server", "7.0", "Maipo"), (7, 0)), 21 | (("Ubuntu", "16.04", "xenial"), (16, 4)), 22 | (("Ubuntu", "19.04", "disco"), (19, 4)), 23 | (("Univention", '"4.4-0 errata175"', "Blumenthal"), (4, 4)), 24 | # TODO: add SLES12 25 | (("openSUSE project", "42.3", "n/a"), (42, 3)), 26 | (("openSUSE", "15.0", "n/a"), (15, 0)), 27 | ] 28 | 29 | 30 | @pytest.mark.parametrize("dist_info, expected_version", DISTRI_INFOS) 31 | def testDistributionHasVersionSet(dist_info, expected_version): 32 | dist = Distribution(distribution_information=dist_info) 33 | 34 | assert dist.version 35 | assert expected_version == dist.version 36 | assert isinstance(dist.version, tuple) 37 | 38 | 39 | @pytest.mark.parametrize("dist_info, expected_version", DISTRI_INFOS) 40 | def testDistributionReprContainsAllValues(dist_info, expected_version): 41 | dist = Distribution(distribution_information=dist_info) 42 | 43 | for part in dist_info: 44 | assert part.strip() in repr(dist) 45 | 46 | 47 | @pytest.mark.parametrize("dist_info, expected_version", DISTRI_INFOS) 48 | def testDistributionNameGetsWhitespaceStripped(dist_info, expected_version): 49 | dist = Distribution(distribution_information=dist_info) 50 | 51 | assert dist.distribution == dist_info[0].strip() 52 | 53 | 54 | @pytest.mark.parametrize("dist_info, expected_version", DISTRI_INFOS) 55 | def testDistributionHasDistributorSet(dist_info, expected_version): 56 | dist = Distribution(distribution_information=dist_info) 57 | 58 | assert dist.distributor 59 | -------------------------------------------------------------------------------- /tests/test_util_file_archive.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing the work with archives. 7 | """ 8 | 9 | import pytest 10 | 11 | from OPSI.Util.File.Archive import Archive, is_pigz_available 12 | 13 | from .helpers import mock 14 | 15 | 16 | def testArchiveFactoryRaisesExceptionOnUnknownFormat(): 17 | with pytest.raises(Exception): 18 | Archive("no_filename", format="unknown") 19 | 20 | 21 | @pytest.mark.parametrize("fileFormat", ["tar", "cpio"]) 22 | def testCreatingArchive(fileFormat): 23 | Archive("no_file", format=fileFormat) 24 | 25 | 26 | def testRaisingExceptionIfFiletypeCanNotBeDetermined(): 27 | with pytest.raises(Exception): 28 | Archive(__file__) 29 | 30 | 31 | def testDisablingPigz(): 32 | """ 33 | Disabling the usage of pigz by setting PIGZ_ENABLED to False. 34 | """ 35 | with mock.patch("OPSI.Util.File.Opsi.OpsiConfFile.isPigzEnabled", lambda x: False): 36 | assert is_pigz_available() is False 37 | -------------------------------------------------------------------------------- /tests/test_util_file_dhcpdconf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing the work with the DHCPD configuration files. 7 | """ 8 | 9 | import codecs 10 | import os 11 | 12 | import pytest 13 | 14 | from OPSI.Util.File import DHCPDConfFile 15 | 16 | from .helpers import createTemporaryTestfile 17 | 18 | 19 | def testParsingExampleDHCPDConf(test_data_path): 20 | testExample = os.path.join(test_data_path, "util", "dhcpd", "dhcpd_1.conf") 21 | 22 | with createTemporaryTestfile(testExample) as fileName: 23 | confFile = DHCPDConfFile(fileName) 24 | confFile.parse() 25 | 26 | 27 | @pytest.fixture 28 | def dhcpdConf(tempDir): 29 | """Mixin for an DHCPD backend. 30 | Manages a subnet 192.168.99.0/24""" 31 | 32 | testData = """ 33 | ddns-update-style none; 34 | default-lease-time 68400; 35 | # max-lease-time 68400; 36 | max-lease-time 68400; 37 | authoritative ; 38 | log-facility local7; 39 | use-host-decl-names on; 40 | option domain-name "domain.local"; 41 | option domain-name-servers ns.domain.local; 42 | option routers 192.168.99.254; 43 | 44 | # Comment netbios name servers 45 | option netbios-name-servers 192.168.99.2; 46 | 47 | subnet 192.168.99.0 netmask 255.255.255.0 { 48 | group { 49 | # Opsi hosts 50 | next-server 192.168.99.2; 51 | filename "linux/pxelinux.0/xxx?{}"; 52 | host opsi-test { 53 | hardware ethernet 9a:e5:3c:10:22:22; 54 | fixed-address opsi-test.domain.local; 55 | } 56 | } 57 | host out-of-group { 58 | hardware ethernet 9a:e5:3c:10:22:22; 59 | fixed-address out-of-group.domain.local; 60 | } 61 | } 62 | host out-of-subnet { 63 | hardware ethernet 1a:25:31:11:23:21; 64 | fixed-address out-of-subnet.domain.local; 65 | } 66 | """ 67 | 68 | dhcpdConfFile = os.path.join(tempDir, "dhcpd.conf") 69 | 70 | with codecs.open(dhcpdConfFile, "w", "utf-8") as f: 71 | f.write(testData) 72 | 73 | yield DHCPDConfFile(dhcpdConfFile) 74 | 75 | 76 | def testAddingHostsToConfig(dhcpdConf): 77 | """ 78 | Adding hosts to a DHCPDConf. 79 | 80 | If this fails on your machine with a message that 127.x.x.x is refused 81 | as network address please correct your hostname settings. 82 | """ 83 | dhcpdConf.parse() 84 | 85 | dhcpdConf.addHost( 86 | "TestclienT", "0001-21-21:00:00", "192.168.99.112", "192.168.99.112", None 87 | ) 88 | dhcpdConf.addHost( 89 | "TestclienT2", 90 | "00:01:09:08:99:11", 91 | "192.168.99.113", 92 | "192.168.99.113", 93 | {"next-server": "192.168.99.2", "filename": "linux/pxelinux.0/xxx?{}"}, 94 | ) 95 | 96 | assert dhcpdConf.getHost("TestclienT2") is not None 97 | assert dhcpdConf.getHost("notthere") is None 98 | 99 | 100 | def testGeneratingConfig(dhcpdConf): 101 | dhcpdConf.parse() 102 | 103 | dhcpdConf.addHost( 104 | "TestclienT", "0001-21-21:00:00", "192.168.99.112", "192.168.99.112", None 105 | ) 106 | dhcpdConf.addHost( 107 | "TestclienT2", 108 | "00:01:09:08:99:11", 109 | "192.168.99.113", 110 | "192.168.99.113", 111 | {"next-server": "192.168.99.2", "filename": "linux/pxelinux.0/xxx?{}"}, 112 | ) 113 | 114 | dhcpdConf.generate() 115 | # TODO: check generated file 116 | -------------------------------------------------------------------------------- /tests/test_util_file_opsi_opsirc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing .opsirc handling. 7 | """ 8 | 9 | import codecs 10 | import os 11 | import pytest 12 | 13 | from OPSI.Util.File.Opsi.Opsirc import getOpsircPath, readOpsirc 14 | from OPSI.Util import randomString 15 | 16 | 17 | @pytest.fixture 18 | def filename(tempDir): 19 | return os.path.join(tempDir, randomString(8) + ".conf") 20 | 21 | 22 | def testReadingMissingFileReturnsNoConfig(tempDir): 23 | assert {} == readOpsirc("iamnothere") 24 | 25 | 26 | def testReadingEmptyConfigFile(filename): 27 | with open(filename, "w"): 28 | pass 29 | 30 | config = readOpsirc(filename) 31 | 32 | assert {} == config 33 | 34 | 35 | def testReadingConfigFile(filename): 36 | with open(filename, "w") as f: 37 | f.write("address = https://lullaby.machine.dream:12345/c3\n") 38 | f.write("username = hanz\n") 39 | f.write("password = gr3tel\n") 40 | 41 | config = readOpsirc(filename) 42 | 43 | assert len(config) == 3 44 | assert config["address"] == "https://lullaby.machine.dream:12345/c3" 45 | assert config["username"] == "hanz" 46 | assert config["password"] == "gr3tel" 47 | 48 | 49 | def testReadingConfigFileIgnoresLeadingAndTrailingSpacing(filename): 50 | with open(filename, "w") as f: 51 | f.write(" address = https://lullaby.machine.dream:12345/c3\n") 52 | f.write("username=hanz \n") 53 | f.write(" password = gr3tel \n") 54 | 55 | config = readOpsirc(filename) 56 | 57 | assert len(config) == 3 58 | assert config["address"] == "https://lullaby.machine.dream:12345/c3" 59 | assert config["username"] == "hanz" 60 | assert config["password"] == "gr3tel" 61 | 62 | 63 | def testReadingPasswordFromCredentialsfile(filename): 64 | password = randomString(32) 65 | 66 | pwdfile = filename + ".secret" 67 | with codecs.open(pwdfile, "w", "utf-8") as f: 68 | f.write(password + "\n") 69 | 70 | with open(filename, "w") as f: 71 | f.write("address = https://lullaby.machine.dream:12345/c3\n") 72 | f.write("username = hanz\n") 73 | f.write("password file = {}\n".format(pwdfile)) 74 | 75 | config = readOpsirc(filename) 76 | 77 | assert len(config) == 3 78 | assert config["address"] == "https://lullaby.machine.dream:12345/c3" 79 | assert config["username"] == "hanz" 80 | assert config["password"] == password 81 | 82 | 83 | def testIgnoringComments(filename): 84 | with open(filename, "w") as f: 85 | f.write(";address = https://bad.guy.dream:12345/c3\n") 86 | f.write("# address = https://blue.pill.dream:12345/c3\n") 87 | f.write("address = https://lullaby.machine.dream:12345/c3\n") 88 | f.write(" # address = https://last.one.neo:12345/c3\n") 89 | 90 | config = readOpsirc(filename) 91 | 92 | assert len(config) == 1 93 | assert config["address"] == "https://lullaby.machine.dream:12345/c3" 94 | 95 | 96 | def testIgnoringUnknownKeywords(filename): 97 | with open(filename, "w") as f: 98 | f.write("hello = world\n") 99 | f.write("i coded = a bot\n") 100 | f.write("and I liked it\n") 101 | 102 | config = readOpsirc(filename) 103 | 104 | assert not config 105 | 106 | 107 | def testIgnoringEmptyValues(filename): 108 | with open(filename, "w") as f: 109 | f.write("username=\n") 110 | f.write("username = foo\n") 111 | f.write("username = \n") 112 | 113 | config = readOpsirc(filename) 114 | 115 | assert len(config) == 1 116 | assert config["username"] == "foo" 117 | 118 | 119 | def testReadingOpsircPath(): 120 | path = getOpsircPath() 121 | assert "~" not in path 122 | 123 | head, tail = os.path.split(path) 124 | assert tail == "opsirc" 125 | assert head.endswith(".opsi.org") 126 | -------------------------------------------------------------------------------- /tests/test_util_product.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing the OPSI.Util.Product module. 7 | """ 8 | 9 | import os 10 | import re 11 | import tempfile 12 | 13 | import pytest 14 | 15 | import OPSI.Util.Product as Product 16 | 17 | from .helpers import cd, mock 18 | 19 | 20 | @pytest.mark.parametrize( 21 | "text", 22 | [ 23 | ".svn", 24 | pytest.param(".svnotmatching", marks=pytest.mark.xfail), 25 | ".git", 26 | pytest.param(".gitignore", marks=pytest.mark.xfail), 27 | ], 28 | ) 29 | def testDirectoryExclusion(text): 30 | assert re.match(Product.EXCLUDE_DIRS_ON_PACK_REGEX, text) is not None 31 | 32 | 33 | def testProductPackageFileRemovingFolderWithUnicodeFilenamesInsideFails(tempDir): 34 | """ 35 | As mentioned in http://bugs.python.org/issue3616 the attempt to 36 | remove a filename that contains unicode can fail. 37 | 38 | Sometimes products are created that feature files with filenames 39 | that do containt just that. 40 | We need to make shure that removing such fails does not fail and 41 | that we are able to remove them. 42 | """ 43 | tempPackageFilename = tempfile.NamedTemporaryFile(suffix=".opsi") 44 | 45 | ppf = Product.ProductPackageFile(tempPackageFilename.name) 46 | ppf.setClientDataDir(tempDir) 47 | 48 | fakeProduct = mock.Mock() 49 | fakeProduct.getId.return_value = "umlauts" 50 | fakePackageControlFile = mock.Mock() 51 | fakePackageControlFile.getProduct.return_value = fakeProduct 52 | 53 | # Setting up evil file 54 | targetDir = os.path.join(tempDir, "umlauts") 55 | os.makedirs(targetDir) 56 | 57 | with cd(targetDir): 58 | os.system(r"touch -- $(echo -e '--\0250--')") 59 | 60 | with mock.patch.object(ppf, "packageControlFile", fakePackageControlFile): 61 | ppf.deleteProductClientDataDir() 62 | 63 | assert not os.path.exists( 64 | targetDir 65 | ), "Product directory in depot should be deleted." 66 | 67 | 68 | def testSettigUpProductPackageFileWithNonExistingFileFails(): 69 | with pytest.raises(Exception): 70 | Product.ProductPackageFile("nonexisting.opsi") 71 | 72 | 73 | def testCreatingProductPackageSourceRequiresExistingSourceFolder(tempDir): 74 | targetDir = os.path.join(tempDir, "nope") 75 | 76 | with pytest.raises(Exception): 77 | Product.ProductPackageSource(targetDir) 78 | -------------------------------------------------------------------------------- /tests/test_util_task_backup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing the task to backup opsi. 7 | """ 8 | 9 | import os 10 | import shutil 11 | import sys 12 | 13 | from OPSI.Util.Task.Backup import OpsiBackup 14 | from OPSI.Util.Task.ConfigureBackend import getBackendConfiguration, updateConfigFile 15 | 16 | from .helpers import mock, workInTemporaryDirectory 17 | 18 | try: 19 | from .Backends.MySQL import MySQLconfiguration 20 | except ImportError: 21 | MySQLconfiguration = None 22 | 23 | 24 | def testVerifySysConfigDoesNotFailBecauseWhitespaceAtEnd(): 25 | backup = OpsiBackup() 26 | 27 | archive = { 28 | "distribution": "SUSE Linux Enterprise Server", 29 | "sysVersion": "(12, 0)", 30 | } 31 | system = { 32 | "distribution": "SUSE Linux Enterprise Server ", # note the extra space 33 | "sysVersion": (12, 0), 34 | } 35 | 36 | assert {} == backup.getDifferencesInSysConfig(archive, sysInfo=system) 37 | 38 | 39 | def testPatchingStdout(): 40 | fake = "fake" 41 | backup = OpsiBackup(stdout=fake) 42 | assert fake == backup.stdout 43 | 44 | newBackup = OpsiBackup() 45 | assert sys.stdout == newBackup.stdout 46 | 47 | 48 | def testGettingArchive(dist_data_path): 49 | fakeBackendDir = os.path.join(dist_data_path, "backends") 50 | fakeBackendDir = os.path.normpath(fakeBackendDir) 51 | 52 | with mock.patch( 53 | "OPSI.Util.Task.Backup.OpsiBackupArchive.BACKEND_CONF_DIR", fakeBackendDir 54 | ): 55 | backup = OpsiBackup() 56 | archive = backup._getArchive("r") 57 | 58 | assert os.path.exists(archive.name), "No archive created." 59 | os.remove(archive.name) 60 | 61 | 62 | def testCreatingArchive(dist_data_path): 63 | with workInTemporaryDirectory() as backendDir: 64 | with workInTemporaryDirectory() as tempDir: 65 | assert 0 == len(os.listdir(tempDir)), "Directory not empty" 66 | 67 | configDir = os.path.join(backendDir, "config") 68 | os.mkdir(configDir) 69 | 70 | sourceBackendDir = os.path.join(dist_data_path, "backends") 71 | sourceBackendDir = os.path.normpath(sourceBackendDir) 72 | fakeBackendDir = os.path.join(backendDir, "backends") 73 | 74 | shutil.copytree(sourceBackendDir, fakeBackendDir) 75 | 76 | for filename in os.listdir(fakeBackendDir): 77 | if not filename.endswith(".conf"): 78 | continue 79 | 80 | configPath = os.path.join(fakeBackendDir, filename) 81 | config = getBackendConfiguration(configPath) 82 | if "file" in filename: 83 | config["baseDir"] = configDir 84 | elif "mysql" in filename and MySQLconfiguration: 85 | config.update(MySQLconfiguration) 86 | else: 87 | continue # no modifications here 88 | 89 | updateConfigFile(configPath, config) 90 | 91 | with mock.patch( 92 | "OPSI.Util.Task.Backup.OpsiBackupArchive.CONF_DIR", 93 | os.path.dirname(__file__), 94 | ): 95 | with mock.patch( 96 | "OPSI.Util.Task.Backup.OpsiBackupArchive.BACKEND_CONF_DIR", 97 | fakeBackendDir, 98 | ): 99 | backup = OpsiBackup() 100 | backup.create() 101 | 102 | dirListing = os.listdir(tempDir) 103 | try: 104 | dirListing.remove(".coverage") 105 | except ValueError: 106 | pass 107 | 108 | assert len(dirListing) == 1 109 | -------------------------------------------------------------------------------- /tests/test_util_task_cleanup_backend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing backend cleaning. 7 | """ 8 | 9 | from OPSI.Object import LocalbootProduct, ProductOnDepot 10 | from OPSI.Util.Task.CleanupBackend import cleanupBackend, cleanUpProducts 11 | 12 | from .test_backend_replicator import ( 13 | checkIfBackendIsFilled, 14 | fillBackend, 15 | fillBackendWithHosts, 16 | ) 17 | 18 | 19 | def testCleanupBackend(cleanableDataBackend): 20 | # TODO: we need checks to see what get's removed and what not. 21 | # TODO: we also should provide some senseless data that will be removed! 22 | fillBackend(cleanableDataBackend) 23 | 24 | cleanupBackend(cleanableDataBackend) 25 | checkIfBackendIsFilled(cleanableDataBackend) 26 | 27 | 28 | def testCleaninUpProducts(cleanableDataBackend): 29 | productIdToClean = "dissection" 30 | 31 | prod1 = LocalbootProduct(productIdToClean, 1, 1) 32 | prod12 = LocalbootProduct(productIdToClean, 1, 2) 33 | prod13 = LocalbootProduct(productIdToClean, 1, 3) 34 | prod2 = LocalbootProduct(productIdToClean + "2", 2, 1) 35 | prod3 = LocalbootProduct("unhallowed", 3, 1) 36 | prod32 = LocalbootProduct("unhallowed", 3, 2) 37 | 38 | products = [prod1, prod12, prod13, prod2, prod3, prod32] 39 | for p in products: 40 | cleanableDataBackend.product_insertObject(p) 41 | 42 | configServer, depotServer, _ = fillBackendWithHosts(cleanableDataBackend) 43 | depot = depotServer[0] 44 | 45 | pod1 = ProductOnDepot( 46 | prod13.id, 47 | prod13.getType(), 48 | prod13.productVersion, 49 | prod13.packageVersion, 50 | configServer.id, 51 | ) 52 | pod1d = ProductOnDepot( 53 | prod13.id, 54 | prod13.getType(), 55 | prod13.productVersion, 56 | prod13.packageVersion, 57 | depot.id, 58 | ) 59 | pod2 = ProductOnDepot( 60 | prod2.id, prod2.getType(), prod2.productVersion, prod2.packageVersion, depot.id 61 | ) 62 | pod3 = ProductOnDepot( 63 | prod3.id, prod3.getType(), prod3.productVersion, prod3.packageVersion, depot.id 64 | ) 65 | 66 | for pod in [pod1, pod1d, pod2, pod3]: 67 | cleanableDataBackend.productOnDepot_insertObject(pod) 68 | 69 | cleanUpProducts(cleanableDataBackend) 70 | 71 | products = cleanableDataBackend.product_getObjects(id=productIdToClean) 72 | assert len(products) == 1 73 | 74 | product = products[0] 75 | assert product.id == productIdToClean 76 | 77 | allProducts = cleanableDataBackend.product_getObjects() 78 | assert len(allProducts) == 3 # prod13, prod2, prod3 79 | -------------------------------------------------------------------------------- /tests/test_util_task_initialize_backend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing the backend initialisation. 7 | """ 8 | 9 | import OPSI.Util.Task.InitializeBackend as initBackend 10 | 11 | 12 | def testGettingServerConfig(): 13 | networkConfig = { 14 | "ipAddress": "192.168.12.34", 15 | "hardwareAddress": "acabacab", 16 | "subnet": "192.168.12.0", 17 | "netmask": "255.255.255.0", 18 | } 19 | fqdn = "blackwidow.test.invalid" 20 | 21 | config = initBackend._getServerConfig(fqdn, networkConfig) 22 | 23 | assert config["id"] == fqdn 24 | for key in ( 25 | "opsiHostKey", 26 | "description", 27 | "notes", 28 | "inventoryNumber", 29 | "masterDepotId", 30 | ): 31 | assert config[key] is None 32 | 33 | assert config["ipAddress"] == networkConfig["ipAddress"] 34 | assert config["hardwareAddress"] == networkConfig["hardwareAddress"] 35 | assert config["maxBandwidth"] == 0 36 | assert config["isMasterDepot"] is True 37 | assert config["depotLocalUrl"] == "file:///var/lib/opsi/depot" 38 | assert config["depotRemoteUrl"] == f"smb://{fqdn}/opsi_depot" 39 | assert config["depotWebdavUrl"] == f"webdavs://{fqdn}:4447/depot" 40 | assert config["repositoryLocalUrl"] == "file:///var/lib/opsi/repository" 41 | assert config["repositoryRemoteUrl"] == f"webdavs://{fqdn}:4447/repository" 42 | assert config["workbenchLocalUrl"] == "file:///var/lib/opsi/workbench" 43 | assert config["workbenchRemoteUrl"] == f"smb://{fqdn}/opsi_workbench" 44 | assert ( 45 | config["networkAddress"] 46 | == f"{networkConfig['subnet']}/{networkConfig['netmask']}" 47 | ) 48 | -------------------------------------------------------------------------------- /tests/test_util_task_update_backend_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing the update of configuration data. 7 | """ 8 | 9 | import pytest 10 | 11 | from OPSI.Object import OpsiClient, OpsiDepotserver, OpsiConfigserver 12 | from OPSI.Util.Task.UpdateBackend.ConfigurationData import updateBackendData 13 | 14 | from .test_hosts import getLocalHostFqdn 15 | from .helpers import mock 16 | 17 | 18 | @pytest.mark.parametrize( 19 | "onSuse, expectedLocalPath", 20 | ( 21 | (True, "file:///var/lib/opsi/workbench"), 22 | (False, "file:///home/opsiproducts"), 23 | ), 24 | ) 25 | def testUpdateBackendData(backendManager, onSuse, expectedLocalPath): 26 | def getDepotAddress(address): 27 | _, addressAndPath = address.split(":") 28 | return addressAndPath.split("/")[2] 29 | 30 | addServers(backendManager) 31 | with mock.patch( 32 | "OPSI.Util.Task.UpdateBackend.ConfigurationData.isOpenSUSE", lambda: onSuse 33 | ): 34 | with mock.patch( 35 | "OPSI.Util.Task.UpdateBackend.ConfigurationData.isSLES", lambda: onSuse 36 | ): 37 | updateBackendData(backendManager) 38 | 39 | servers = backendManager.host_getObjects( 40 | type=["OpsiDepotserver", "OpsiConfigserver"] 41 | ) 42 | assert servers, "No servers found in backend." 43 | 44 | for server in servers: 45 | assert server.workbenchLocalUrl == expectedLocalPath 46 | 47 | depotAddress = getDepotAddress(server.depotRemoteUrl) 48 | expectedAddress = "smb://" + depotAddress + "/opsi_workbench" 49 | assert expectedAddress == server.workbenchRemoteUrl 50 | 51 | 52 | def addServers(backend): 53 | localHostFqdn = getLocalHostFqdn() 54 | configServer = OpsiConfigserver( 55 | id=localHostFqdn, depotRemoteUrl="smb://192.168.123.1/opsi_depot" 56 | ) 57 | backend.host_createObjects([configServer]) 58 | 59 | _, domain = localHostFqdn.split(".", 1) 60 | 61 | def getDepotRemoteUrl(index): 62 | if index % 2 == 0: 63 | return "smb://192.168.123.{}/opsi_depot".format(index) 64 | else: 65 | return "smb://somename/opsi_depot" 66 | 67 | depots = [ 68 | OpsiDepotserver( 69 | id="depot{n}.{domain}".format(n=index, domain=domain), 70 | depotRemoteUrl=getDepotRemoteUrl(index), 71 | ) 72 | for index in range(10) 73 | ] 74 | backend.host_createObjects(depots) 75 | 76 | clients = [ 77 | OpsiClient(id="client{n}.{domain}".format(n=index, domain=domain)) 78 | for index in range(10) 79 | ] 80 | backend.host_createObjects(clients) 81 | -------------------------------------------------------------------------------- /tests/test_util_task_update_backend_file.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing the update of the MySQL backend from an older version. 7 | """ 8 | 9 | import json 10 | import os.path 11 | 12 | import pytest 13 | 14 | from OPSI.Util.Task.UpdateBackend.File import ( 15 | FileBackendUpdateError, 16 | getVersionFilePath, 17 | readBackendVersion, 18 | _readVersionFile, 19 | updateBackendVersion, 20 | updateFileBackend, 21 | ) 22 | 23 | from .Backends.File import getFileBackend 24 | 25 | 26 | @pytest.fixture 27 | def fileBackend(tempDir): 28 | with getFileBackend(path=tempDir) as backend: 29 | yield backend 30 | 31 | 32 | def testUpdatingFileBackend(fileBackend, tempDir): 33 | config = os.path.join(tempDir, "etc", "opsi", "backends", "file.conf") 34 | 35 | updateFileBackend(config) 36 | 37 | 38 | def testReadingSchemaVersionFromMissingFile(tempDir): 39 | assert readBackendVersion(os.path.join(tempDir, "missingbasedir")) is None 40 | 41 | 42 | @pytest.fixture 43 | def baseDirectory(tempDir): 44 | newDir = os.path.join(tempDir, "config") 45 | os.makedirs(newDir) 46 | yield newDir 47 | 48 | 49 | @pytest.fixture 50 | def writtenConfig(baseDirectory): 51 | configFile = getVersionFilePath(baseDirectory) 52 | config = { 53 | 0: { 54 | "start": 1495529319.022833, 55 | "end": 1495529341.870662, 56 | }, 57 | 1: {"start": 1495539432.271123, "end": 1495539478.045244}, 58 | } 59 | with open(configFile, "w") as f: 60 | json.dump(config, f) 61 | 62 | yield config 63 | 64 | 65 | def testReadingSchemaVersionLowLevel(baseDirectory, writtenConfig): 66 | assert writtenConfig == _readVersionFile(baseDirectory) 67 | 68 | 69 | def testReadingSchemaVersion(baseDirectory, writtenConfig): 70 | version = readBackendVersion(baseDirectory) 71 | assert version is not None 72 | assert version == max(writtenConfig.keys()) 73 | assert version > 0 74 | 75 | 76 | @pytest.mark.parametrize( 77 | "config", 78 | [ 79 | {0: {"start": 1495529319.022833}}, # missing end 80 | {0: {}}, # missing start 81 | ], 82 | ) 83 | def testRaisingExceptionOnUnfinishedUpdate(baseDirectory, config): 84 | configFile = getVersionFilePath(baseDirectory) 85 | 86 | with open(configFile, "w") as f: 87 | json.dump(config, f) 88 | 89 | with pytest.raises(FileBackendUpdateError): 90 | readBackendVersion(baseDirectory) 91 | 92 | 93 | def testApplyingTheSameUpdateMultipleTimesFails(baseDirectory): 94 | with updateBackendVersion(baseDirectory, 1): 95 | pass 96 | 97 | with pytest.raises(FileBackendUpdateError): 98 | with updateBackendVersion(baseDirectory, 1): 99 | pass 100 | -------------------------------------------------------------------------------- /tests/test_util_wim.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing working with WIM files. 7 | """ 8 | 9 | import os.path 10 | from contextlib import contextmanager 11 | 12 | import pytest 13 | 14 | from OPSI.Util.WIM import getImageInformation, parseWIM 15 | 16 | from .helpers import workInTemporaryDirectory, mock 17 | 18 | 19 | @contextmanager 20 | def fakeWIMEnvironment(tempDir=None): 21 | from .conftest import TEST_DATA_PATH 22 | 23 | with workInTemporaryDirectory(tempDir) as temporaryDir: 24 | fakeWimPath = os.path.join(temporaryDir, "fake.wim") 25 | with open(fakeWimPath, "w"): 26 | pass 27 | 28 | exampleData = os.path.join(TEST_DATA_PATH, "wimlib.example") 29 | 30 | def fakeReturningOutput(_unused): 31 | with open(exampleData, "rt", encoding="utf-8") as f: 32 | content = f.read() 33 | return content.split("\n") 34 | 35 | with mock.patch("OPSI.Util.WIM.which", lambda x: "/usr/bin/echo"): 36 | with mock.patch("OPSI.Util.WIM.execute", fakeReturningOutput): 37 | yield fakeWimPath 38 | 39 | 40 | @pytest.fixture 41 | def fakeWimPath(dist_data_path): 42 | with fakeWIMEnvironment(dist_data_path) as fakeWimPath: 43 | yield fakeWimPath 44 | 45 | 46 | def testParsingNonExistingWimFileFails(): 47 | with pytest.raises(OSError): 48 | parseWIM("not_here.wim") 49 | 50 | 51 | def testParsingWIMReturnNoInformationFails(fakeWimPath): 52 | with mock.patch("OPSI.Util.WIM.execute", lambda x: [""]): 53 | with pytest.raises(ValueError): 54 | parseWIM(fakeWimPath) 55 | 56 | 57 | def testParsingWIM(fakeWimPath): 58 | imageData = { 59 | "Windows 7 STARTERN": (set(["de-DE"]), "de-DE"), 60 | "Windows 7 HOMEBASICN": (set(["de-DE"]), "de-DE"), 61 | "Windows 7 HOMEPREMIUMN": (set(["de-DE"]), "de-DE"), 62 | "Windows 7 PROFESSIONALN": (set(["de-DE"]), "de-DE"), 63 | "Windows 7 ULTIMATEN": (set(["de-DE"]), "de-DE"), 64 | } 65 | 66 | for image in parseWIM(fakeWimPath): 67 | assert image.name in imageData 68 | 69 | assert image.languages == imageData[image.name][0] 70 | assert image.default_language == imageData[image.name][1] 71 | 72 | del imageData[image.name] 73 | 74 | assert not imageData, "Missed reading info for {0}".format(imageData.keys()) 75 | 76 | 77 | def testReadingImageInformationFromWim(fakeWimPath): 78 | infos = getImageInformation(fakeWimPath) 79 | 80 | for index in range(5): 81 | print("Check #{}...".format(index)) 82 | info = next(infos) 83 | assert info 84 | 85 | with pytest.raises(StopIteration): # Only five infos in example. 86 | next(infos) 87 | -------------------------------------------------------------------------------- /tests/test_util_windows_drivers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) uib GmbH 4 | # License: AGPL-3.0 5 | """ 6 | Testing WindowsDrivers. 7 | """ 8 | 9 | import os 10 | import pytest 11 | 12 | from OPSI.Util.WindowsDrivers import integrateAdditionalWindowsDrivers 13 | from OPSI.Object import AuditHardwareOnHost 14 | 15 | 16 | def auditHardwareOnHostFactory(hardwareClass, hostId, vendor, model, sku=None): 17 | auditHardwareOnHost = AuditHardwareOnHost(hardwareClass, hostId) 18 | auditHardwareOnHost.vendor = vendor 19 | auditHardwareOnHost.model = model 20 | auditHardwareOnHost.sku = sku 21 | 22 | return auditHardwareOnHost 23 | 24 | 25 | def _generateDirectories(folder, vendor, model): 26 | rulesDir = os.path.join(folder, "byAudit") 27 | if not os.path.exists(rulesDir): 28 | os.mkdir(rulesDir) 29 | vendorDir = os.path.join(rulesDir, vendor) 30 | modelDir = os.path.join(vendorDir, model) 31 | 32 | os.mkdir(vendorDir) 33 | os.mkdir(modelDir) 34 | 35 | 36 | def _generateTestFiles(folder, vendor, model, filename): 37 | dstFilename = os.path.join(folder, "byAudit", vendor, model, filename) 38 | with open(dstFilename, "w"): 39 | pass 40 | 41 | 42 | @pytest.fixture 43 | def destinationDir(tempDir): 44 | yield os.path.join(tempDir, "destination") 45 | 46 | 47 | @pytest.fixture(scope="session") 48 | def hostId(): 49 | yield "test.domain.local" 50 | 51 | 52 | @pytest.fixture(scope="session") 53 | def hardwareClass(): 54 | yield "COMPUTER_SYSTEM" 55 | 56 | 57 | def testByAudit(tempDir, destinationDir, hardwareClass, hostId): 58 | vendor = "Dell Inc." 59 | model = "Venue 11 Pro 7130 MS" 60 | 61 | testData1 = auditHardwareOnHostFactory(hardwareClass, hostId, vendor, model) 62 | _generateDirectories(tempDir, vendor, model) 63 | _generateTestFiles(tempDir, vendor, model, "test.inf") 64 | 65 | result = integrateAdditionalWindowsDrivers( 66 | tempDir, destinationDir, [], auditHardwareOnHosts=[testData1] 67 | ) 68 | 69 | expectedResult = [ 70 | { 71 | "devices": [], 72 | "directory": "%s/1" % destinationDir, 73 | "driverNumber": 1, 74 | "infFile": "%s/1/test.inf" % destinationDir, 75 | } 76 | ] 77 | 78 | assert expectedResult == result 79 | 80 | 81 | def testByAuditWithUnderscoreAtTheEnd(tempDir, destinationDir, hardwareClass, hostId): 82 | vendor = "Dell Inc_" 83 | model = "Venue 11 Pro 7130 MS" 84 | 85 | testData1 = auditHardwareOnHostFactory(hardwareClass, hostId, "Dell Inc.", model) 86 | _generateDirectories(tempDir, vendor, model) 87 | _generateTestFiles(tempDir, vendor, model, "test.inf") 88 | 89 | result = integrateAdditionalWindowsDrivers( 90 | tempDir, destinationDir, [], auditHardwareOnHosts=[testData1] 91 | ) 92 | 93 | expectedResult = [ 94 | { 95 | "devices": [], 96 | "directory": "%s/1" % destinationDir, 97 | "driverNumber": 1, 98 | "infFile": "%s/1/test.inf" % destinationDir, 99 | } 100 | ] 101 | 102 | assert expectedResult == result 103 | 104 | 105 | def testByAuditWithSKUFallback(tempDir, destinationDir, hardwareClass, hostId): 106 | vendor = "Dell Inc_" 107 | model = "Venue 11 Pro 7130 MS (ABC)" 108 | sku = "ABC" 109 | model_without_sku = "Venue 11 Pro 7130 MS" 110 | 111 | testData1 = auditHardwareOnHostFactory( 112 | hardwareClass, hostId, "Dell Inc.", model, sku 113 | ) 114 | _generateDirectories(tempDir, vendor, model_without_sku) 115 | _generateTestFiles(tempDir, vendor, model_without_sku, "test.inf") 116 | 117 | result = integrateAdditionalWindowsDrivers( 118 | tempDir, destinationDir, [], auditHardwareOnHosts=[testData1] 119 | ) 120 | 121 | expectedResult = [ 122 | { 123 | "devices": [], 124 | "directory": "%s/1" % destinationDir, 125 | "driverNumber": 1, 126 | "infFile": "%s/1/test.inf" % destinationDir, 127 | } 128 | ] 129 | 130 | assert expectedResult == result 131 | --------------------------------------------------------------------------------