def create(self, fileList, baseDir='.', dereference=False): try: fileList = forceUnicodeList(fileList) baseDir = os.path.abspath(forceFilename(baseDir)) dereference = forceBool(dereference) if not os.path.isdir(baseDir): raise IOError(u"Base dir '%s' not found" % baseDir) command = u'%s --create --quiet --verbose --format crc' % System.which( 'cpio') if dereference: command += ' --dereference' if self._compression == 'gzip': if self.pigz_detected: command += ' | %s --rsyncable' % System.which('pigz') else: command += ' | %s --rsyncable' % System.which('gzip') elif self._compression == 'bzip2': command += ' | %s' % System.which('bzip2') command += ' > "%s"' % self._filename self._create(fileList, baseDir, command) except Exception as e: raise RuntimeError(u"Failed to create archive '%s': %s" % (self._filename, e))
def rebootMachine(self, waitSeconds=3): self._isRebootTriggered = True System.setRegistryValue(System.HKEY_LOCAL_MACHINE, "SOFTWARE\\opsi.org\\winst", "RebootRequested", 0) # Running in thread to avoid failure of reboot (device not ready) RebootThread().start()
def rebootMachine(self, waitSeconds=3): self._isRebootTriggered = True if self._controlPipe: try: self._controlPipe.executeRpc("rebootTriggered", True) except Exception as err: # pylint: disable=broad-except logger.debug(err) self.clearRebootRequest() System.reboot(wait=waitSeconds)
def shutdownMachine(self, waitSeconds=3): self._isShutdownTriggered = True if self._controlPipe: try: self._controlPipe.executeRpc("shutdownTriggered", True) except Exception as err: # pylint: disable=broad-except logger.debug(err) self.clearShutdownRequest() System.shutdown(wait=waitSeconds)
def setBlockLogin(self, blockLogin, handleNotifier=True): # pylint: disable=too-many-branches blockLogin = forceBool(blockLogin) changed = self._blockLogin != blockLogin self._blockLogin = blockLogin logger.notice("Block login now set to '%s'", self._blockLogin) if self._blockLogin: if not self._blockLoginEventId: self._blockLoginEventId = timeline.addEvent( title="Blocking login", description="User login blocked", category="block_login", durationEvent=True) if not self._blockLoginNotifierPid and config.get( 'global', 'block_login_notifier'): if handleNotifier and RUNNING_ON_WINDOWS: logger.info("Starting block login notifier app") # Start block login notifier on physical console sessionId = System.getActiveConsoleSessionId() while True: try: self._blockLoginNotifierPid = System.runCommandInSession( command=config.get('global', 'block_login_notifier'), sessionId=sessionId, desktop='winlogon', waitForProcessEnding=False)[2] break except Exception as err: # pylint: disable=broad-except logger.error( "Failed to start block login notifier app: %s", err) break else: if self._blockLoginEventId: timeline.setEventEnd(eventId=self._blockLoginEventId) self._blockLoginEventId = None if handleNotifier and self._blockLoginNotifierPid: try: logger.info( "Terminating block login notifier app (pid %s)", self._blockLoginNotifierPid) System.terminateProcess( processId=self._blockLoginNotifierPid) except Exception as err: # pylint: disable=broad-except logger.warning( "Failed to terminate block login notifier app: %s", err) self._blockLoginNotifierPid = None if changed and self._controlPipe: try: self._controlPipe.executeRpc("blockLogin", self._blockLogin) except Exception as rpc_error: # pylint: disable=broad-except logger.debug(rpc_error)
def run(self): while True: try: System.reboot(0) logger.notice("Reboot initiated") break except Exception as err: # pylint: disable=broad-except # Device not ready? logger.info("Failed to initiate reboot: %s", err) time.sleep(1)
def extract(self, targetPath='.', patterns=[]): try: targetPath = os.path.abspath(forceFilename(targetPath)) patterns = forceUnicodeList(patterns) if not os.path.isdir(targetPath): try: os.mkdir(targetPath) except Exception as e: raise IOError(u"Failed to create target dir '%s': %s" % (targetPath, e)) cat = System.which('cat') if self._compression == 'gzip': if self.pigz_detected: cat = u'%s --stdout --decompress' % ( System.which('pigz'), ) else: cat = System.which('zcat') elif self._compression == 'bzip2': cat = System.which('bzcat') fileCount = 0 for filename in self.content(): match = False if not patterns: match = True else: for pattern in patterns: try: pattern = pattern.replace('*', '.*') if re.search(pattern, filename): match = True break fileCount += 1 except Exception as e: raise ValueError(u"Bad pattern '%s': %s" % (pattern, e)) if match: fileCount += 1 include = ' '.join('"%s"' % pattern for pattern in patterns) curDir = os.path.abspath(os.getcwd()) os.chdir(targetPath) try: command = u'%s "%s" | %s --quiet --extract --make-directories --unconditional --preserve-modification-time --verbose --no-preserve-owner %s' % ( cat, self._filename, System.which('cpio'), include) self._extract(command, fileCount) finally: os.chdir(curDir) except Exception as e: raise RuntimeError(u"Failed to extract archive '%s': %s" % (self._filename, e))
def suspendBitlocker(self): # pylint: disable=no-self-use logger.notice("Suspending bitlocker for one reboot if active") try: System.execute( "powershell.exe -ExecutionPolicy Bypass -Command \"" "foreach($v in Get-BitLockerVolume)" "{if ($v.EncryptionPercentage -gt 0)" "{$v | Suspend-BitLocker -RebootCount 1}}\"", captureStderr=True, waitForEnding=True, timeout=20) except Exception as err: # pylint: disable=broad-except logger.error("Failed to suspend bitlocker: %s", err, exc_info=True)
def deleteActionProcessorUser(self): if not config.get('action_processor', 'delete_user'): return if not self._actionProcessorUserName: return if not System.existsUser(username=self._actionProcessorUserName): return logger.notice("Deleting local user '%s'", self._actionProcessorUserName) System.deleteUser(username=self._actionProcessorUserName) self._actionProcessorUserName = '' self._actionProcessorUserPassword = ''
def is_correct_pigz_version(): ver = System.execute('pigz --version')[0][5:] logger.debug('Detected pigz version: %s' % (ver, )) versionMatches = compareVersions(ver, '>=', '2.2.3') logger.debug('pigz version is compatible? %s' % (versionMatches)) return versionMatches
def _applySystemSpecificConfiguration(self): defaultToApply = self.WINDOWS_DEFAULT_PATHS.copy() if RUNNING_ON_LINUX: defaultToApply = self.LINUX_DEFAULT_PATHS.copy() elif RUNNING_ON_DARWIN: defaultToApply = self.MACOS_DEFAULT_PATHS.copy() baseDir = self._config["global"]["base_dir"] for key in list(self._config): if key in defaultToApply: self._config[key].update(defaultToApply[key]) self._config['cache_service']['extension_config_dir'] = os.path.join( baseDir, 'opsiclientd', 'extend.d') if RUNNING_ON_WINDOWS: systemDrive = System.getSystemDrive() logger.debug( "Running on windows: adapting paths to use system drive (%s)", systemDrive) systemDrive += "\\" self._config['cache_service']['storage_dir'] = os.path.join( systemDrive, 'opsi.org', 'cache') self._config['global']['config_file'] = os.path.join( baseDir, 'opsiclientd', 'opsiclientd.conf') self._config['global']['log_dir'] = os.path.join( systemDrive, 'opsi.org', 'log') self._config['global']['state_file'] = os.path.join( systemDrive, 'opsi.org', 'opsiclientd', 'state.json') self._config['global']['server_cert_dir'] = os.path.join( systemDrive, 'opsi.org', 'tls') self._config['global']['timeline_db'] = os.path.join( systemDrive, 'opsi.org', 'opsiclientd', 'timeline.sqlite') self._config['system'][ 'program_files_dir'] = System.getProgramFilesDir() if sys.getwindowsversion()[0] == 5: # pylint: disable=no-member self._config['action_processor']['run_as_user'] = '******' else: sslCertDir = os.path.join('/etc', 'opsi-client-agent') for certPath in ('ssl_server_key_file', 'ssl_server_cert_file'): if sslCertDir not in self._config['control_server'][certPath]: self._config['control_server'][certPath] = os.path.join( sslCertDir, self._config['control_server'][certPath])
def is_pigz_available(): def is_correct_pigz_version(): ver = System.execute('pigz --version')[0][5:] logger.debug('Detected pigz version: %s' % (ver, )) versionMatches = compareVersions(ver, '>=', '2.2.3') logger.debug('pigz version is compatible? %s' % (versionMatches)) return versionMatches if not PIGZ_ENABLED: return False try: System.which('pigz') logger.debug(u'Detected "pigz".') return is_correct_pigz_version() except Exception: logger.debug(u'Did not detect "pigz".') return False
def isShutdownRequested(self): try: shutdownRequested = System.getRegistryValue( System.HKEY_LOCAL_MACHINE, "SOFTWARE\\opsi.org\\winst", "ShutdownRequested") except Exception as err: # pylint: disable=broad-except logger.info("Failed to get shutdownRequested from registry: %s", err) shutdownRequested = 0 logger.notice("Shutdown request in Registry: %s", shutdownRequested) return forceBool(shutdownRequested)
def content(self): try: if not os.path.exists(self._filename): raise IOError(u"Archive file not found: '%s'" % self._filename) cat = System.which('cat') if self._compression == 'gzip': if self.pigz_detected: cat = u'{pigz} --stdout --decompress'.format( pigz=System.which('pigz')) else: cat = System.which('zcat') elif self._compression == 'bzip2': cat = System.which('bzcat') return [ unicode(line) for line in System.execute( u'{cat} "{filename}" | {cpio} --quiet --extract --list'. format(cat=cat, filename=self._filename, cpio=System.which('cpio'))) if line ] except Exception as e: raise RuntimeError(u"Failed to get archive content '%s': %s" % (self._filename, e))
def createActionProcessorUser(self, recreate=True): if not config.get('action_processor', 'create_user'): return run_as_user = config.get('action_processor', 'run_as_user') if run_as_user.lower() == 'system': self._actionProcessorUserName = '' self._actionProcessorUserPassword = '' return if '\\' in run_as_user: logger.warning( "Ignoring domain part of user to run action processor '%s'", run_as_user) run_as_user = run_as_user.split('\\', -1) if not recreate and self._actionProcessorUserName and self._actionProcessorUserPassword and System.existsUser( username=run_as_user): return self._actionProcessorUserName = run_as_user logger.notice(f"Creating local user '{run_as_user}'") self._actionProcessorUserPassword = '******' + str( randomString(16)) + '!/%' secret_filter.add_secrets(self._actionProcessorUserPassword) if System.existsUser(username=run_as_user): System.deleteUser(username=run_as_user) System.createUser(username=run_as_user, password=self._actionProcessorUserPassword, groups=[System.getAdminGroupName()])
def loginUser(self, username, password): for session_id in System.getActiveSessionIds(protocol="console"): System.lockSession(session_id) for _unused in range(20): if self._controlPipe.credentialProviderConnected( login_capable=True): break time.sleep(0.5) if not self._controlPipe.credentialProviderConnected( login_capable=True): raise RuntimeError( "No login capable opsi credential provider connected") logger.info( "Login capable opsi credential provider connected, calling loginUser" ) for response in self._controlPipe.executeRpc("loginUser", username, password): if not response.get("error") and response.get("result"): return True raise RuntimeError( f"opsi credential provider failed to login user '{username}': {response.get('error')}" )
def getCurrentActiveDesktopName(self, sessionId=None): if not RUNNING_ON_WINDOWS: return None if not ('opsiclientd_rpc' in config.getDict() and 'command' in config.getDict()['opsiclientd_rpc']): raise Exception("opsiclientd_rpc command not defined") if sessionId is None: sessionId = System.getActiveSessionId() if sessionId is None: sessionId = System.getActiveConsoleSessionId() rpc = f"setCurrentActiveDesktopName(\"{sessionId}\", System.getActiveDesktopName())" cmd = config.get('opsiclientd_rpc', 'command') + ' "' + rpc.replace( '"', '\\"') + '"' try: System.runCommandInSession(command=cmd, sessionId=sessionId, desktop="winlogon", waitForProcessEnding=True, timeoutSeconds=60, noWindow=True) except Exception as err: # pylint: disable=broad-except logger.error(err) desktop = self._currentActiveDesktopName.get(sessionId) if not desktop: logger.warning( "Failed to get current active desktop name for session %s, using 'default'", sessionId) desktop = "default" self._currentActiveDesktopName[sessionId] = desktop logger.debug( "Returning current active dektop name '%s' for session %s", desktop, sessionId) return desktop
def content(self): try: if not os.path.exists(self._filename): raise IOError(u"Archive file not found: '%s'" % self._filename) names = [] options = u'' if self._compression == 'gzip': if self.pigz_detected: options += u'--use-compress-program=pigz' else: options += u'--gunzip' elif self._compression == 'bzip2': options += u'--bzip2' for line in System.execute( u'%s %s --list --file "%s"' % (System.which('tar'), options, self._filename)): if line: names.append(unicode(line)) return names except Exception as e: raise RuntimeError(u"Failed to get archive content '%s': %s" % (self._filename, e))
def switchDesktop(self, desktop, sessionId=None): # pylint: disable=no-self-use if not ('opsiclientd_rpc' in config.getDict() and 'command' in config.getDict()['opsiclientd_rpc']): raise Exception("opsiclientd_rpc command not defined") desktop = forceUnicode(desktop) if sessionId is None: sessionId = System.getActiveSessionId() if sessionId is None: sessionId = System.getActiveConsoleSessionId() sessionId = forceInt(sessionId) rpc = f"noop(System.switchDesktop('{desktop}'))" cmd = f'{config.get("opsiclientd_rpc", "command")} "{rpc}"' try: System.runCommandInSession(command=cmd, sessionId=sessionId, desktop=desktop, waitForProcessEnding=True, timeoutSeconds=60, noWindow=True) except Exception as err: # pylint: disable=broad-except logger.error(err)
def run(self): self._reloadEvent.clear() self._reloadEvent.wait(2) with self._reloadLock: try: result = System.execute(self._reloadConfigCommand) for line in result: if 'error' in line: raise RuntimeError(u'\n'.join(result)) except Exception as error: logger.critical( u"Failed to restart dhcpd: {0}".format(error)) self._reloadEvent.set()
def extract(self, targetPath='.', patterns=[]): try: targetPath = os.path.abspath(forceFilename(targetPath)) patterns = forceUnicodeList(patterns) if not os.path.isdir(targetPath): try: os.mkdir(targetPath) except Exception as e: raise IOError(u"Failed to create target dir '%s': %s" % (targetPath, e)) options = u'' if self._compression == 'gzip': if self.pigz_detected: options += u'--use-compress-program=pigz' else: options += u'--gunzip' elif self._compression == 'bzip2': options += u'--bzip2' fileCount = 0 for filename in self.content(): match = False if not patterns: match = True else: for pattern in patterns: try: pattern = pattern.replace('*', '.*') if re.search(pattern, filename): match = True break fileCount += 1 except Exception as e: raise ValueError(u"Bad pattern '%s': %s" % (pattern, e)) if match: fileCount += 1 else: options += u' --exclude="%s"' % filename command = u'%s %s --directory "%s" --extract --verbose --file "%s"' % ( System.which('tar'), options, targetPath, self._filename) self._extract(command, fileCount) except Exception as e: raise RuntimeError(u"Failed to extract archive '%s': %s" % (self._filename, e))
def isRebootRequested(self): try: rebootRequested = System.getRegistryValue( System.HKEY_LOCAL_MACHINE, "SOFTWARE\\opsi.org\\winst", "RebootRequested") except Exception as error: # pylint: disable=broad-except logger.warning("Failed to get RebootRequested from registry: %s", error) rebootRequested = 0 logger.notice("Reboot request in Registry: %s", rebootRequested) if rebootRequested == 2: # Logout logger.info("Logout requested") self.clearRebootRequest() return False return forceBool(rebootRequested)
def get(self, name, default=None): # pylint: disable=too-many-return-statements,too-many-branches name = forceUnicode(name) if name == 'user_logged_in': if RUNNING_ON_WINDOWS: for session in System.getActiveSessionInformation(): if session["UserName"] != OPSI_SETUP_USER_NAME: return True return False if RUNNING_ON_LINUX: for proc in psutil.process_iter(): try: env = proc.environ() if env.get("DISPLAY") and proc.uids()[0] >= 1000: return True except psutil.AccessDenied: pass return False if RUNNING_ON_DARWIN: # TODO return True return False if name == 'products_cached': return self._state.get('product_cache_service', {}).get('products_cached', default) if name == 'config_cached': return self._state.get('config_cache_service', {}).get('config_cached', default) if "cancel_counter" in name: return self._state.get(name, 0) if name == 'installation_pending': return forceBool(self._state.get('installation_pending', False)) try: return self._state[name] except KeyError: logger.warning("Unknown state name '%s', returning default '%s'", name, default) return default
def main(): # pylint: disable=too-many-statements,too-many-branches startup_log("windows.main") log_dir = os.path.join(System.getSystemDrive() + "\\opsi.org\\log") parent = psutil.Process(os.getpid()).parent() parent_name = parent.name() if parent else None # https://stackoverflow.com/questions/25770873/python-windows-service-pyinstaller-executables-error-1053 startup_log(f"argv={sys.argv} parent_name={parent_name}") if len(sys.argv) == 1 and parent_name == "services.exe": startup_log("import start service") from opsiclientd.windows.service import start_service # pylint: disable=import-outside-toplevel startup_log("init logging") init_logging(stderr_level=LOG_NONE, log_dir=log_dir) startup_log("start service") start_service() return if any(arg in sys.argv[1:] for arg in ("install", "update", "remove", "start", "stop", "restart")): from opsiclientd.windows.service import handle_commandline # pylint: disable=import-outside-toplevel handle_commandline() return if any(arg in sys.argv[1:] for arg in ("setup", "--version", "--help")): options = parser.parse_args() if options.config_file: Config().set("global", "config_file", options.config_file) if options.action == "setup": oc_init_logging(stderr_level=options.logLevel, stderr_format=DEFAULT_STDERR_LOG_FORMAT) setup(full=True, options=options) return try: if "--elevated" not in sys.argv and parent_name != "python.exe": executable = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + ".exe" args = " ".join(sys.argv[1:]) command = executable + " " + args + " --elevated" try: run_as_system(command) except Exception as err: # pylint: disable=broad-except print(f"Failed to run {command} as system: {err}", file=sys.stderr) raise return integrity_level = get_integrity_level() if int(integrity_level.split("-")[-1]) < 12288: raise RuntimeError( f"opsiclientd.exe must be run as service or from an elevated cmd.exe (integrity_level={integrity_level})" ) if "--elevated" in sys.argv: sys.argv.remove("--elevated") options = parser.parse_args() if options.config_file: Config().set("global", "config_file", options.config_file) init_logging(log_dir=log_dir, stderr_level=options.logLevel, log_filter=options.logFilter) with opsicommon.logging.log_context({'instance', 'opsiclientd'}): logger.notice("Running as user: %s", win32api.GetUserName()) if parent: logger.notice("Parent process: %s (%s)", parent.name(), parent.pid) logger.debug(os.environ) from .opsiclientd import opsiclientd_factory # pylint: disable=import-outside-toplevel opsiclientd = opsiclientd_factory() try: opsiclientd.start() while True: time.sleep(1) except KeyboardInterrupt: logger.essential("KeyboardInterrupt #1 -> stop") opsiclientd.stop() try: opsiclientd.join(15) except KeyboardInterrupt: logger.essential("KeyboardInterrupt #2 -> kill process") psutil.Process(os.getpid()).kill() except Exception as err: # pylint: disable=broad-except logger.error(err, exc_info=True) time.sleep(3) sys.exit(1)
def clearShutdownRequest(self): System.setRegistryValue(System.HKEY_LOCAL_MACHINE, "SOFTWARE\\opsi.org\\winst", "ShutdownRequested", 0)
def main(): # pylint: disable=too-many-locals,too-many-branches,too-many-statements if len(sys.argv) != 17: print( f"Usage: {os.path.basename(sys.argv[0])} <hostId> <hostKey> <controlServerPort>" " <logFile> <logLevel> <depotRemoteUrl> <depotDrive> <depotServerUsername> <depotServerPassword>" " <sessionId> <actionProcessorDesktop> <actionProcessorCommand> <actionProcessorTimeout>" " <runAsUser> <runAsPassword> <createEnvironment>") sys.exit(1) ( # pylint: disable=unbalanced-tuple-unpacking hostId, hostKey, controlServerPort, logFile, logLevel, depotRemoteUrl, depotDrive, depotServerUsername, depotServerPassword, sessionId, actionProcessorDesktop, actionProcessorCommand, actionProcessorTimeout, runAsUser, runAsPassword, createEnvironment) = sys.argv[1:] if hostKey: secret_filter.add_secrets(hostKey) if depotServerPassword: secret_filter.add_secrets(depotServerPassword) if runAsPassword: secret_filter.add_secrets(runAsPassword) init_logging(stderr_level=LOG_NONE, stderr_format=DEFAULT_STDERR_LOG_FORMAT, log_file=logFile, file_level=int(logLevel), file_format=DEFAULT_FILE_LOG_FORMAT) log_instance = f'{os.path.basename(sys.argv[0]).rsplit(".", 1)[0]}_s{sessionId}' with log_context({'instance': log_instance}): logger.debug( "Called with arguments: %s", ', '.join( (hostId, hostKey, controlServerPort, logFile, logLevel, depotRemoteUrl, depotDrive, depotServerUsername, depotServerPassword, sessionId, actionProcessorDesktop, actionProcessorCommand, actionProcessorTimeout, runAsUser, runAsPassword, createEnvironment))) language = "en" try: language = locale.getdefaultlocale()[0].split('_')[0] except Exception as err: # pylint: disable=broad-except logger.debug("Failed to find default language: %s", err) def _(string): """ Fallback function """ return string sp = None try: logger.debug("Loading translation for language '%s'", language) sp = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if os.path.exists(os.path.join(sp, "site-packages")): sp = os.path.join(sp, "site-packages") sp = os.path.join(sp, 'opsiclientd_data', 'locale') translation = gettext.translation('opsiclientd', sp, [language]) _ = translation.gettext except Exception as err: # pylint: disable=broad-except logger.debug("Failed to load locale for %s from %s: %s", language, sp, err) createEnvironment = bool( runAsUser and createEnvironment.lower() in ('yes', 'true', '1')) actionProcessorTimeout = int(actionProcessorTimeout) imp = None depotShareMounted = False be = None depot_url = urlparse(depotRemoteUrl) try: be = JSONRPCBackend( username=hostId, password=hostKey, address=f"https://127.0.0.1:{controlServerPort}/opsiclientd") if runAsUser: if getpass.getuser().lower() != runAsUser.lower(): logger.info("Impersonating user '%s'", runAsUser) imp = System.Impersonate(username=runAsUser, password=runAsPassword, desktop=actionProcessorDesktop) imp.start(logonType="INTERACTIVE", newDesktop=False, createEnvironment=createEnvironment) elif depot_url.scheme in ("smb", "cifs"): logger.info("Impersonating network account '%s'", depotServerUsername) imp = System.Impersonate(username=depotServerUsername, password=depotServerPassword, desktop=actionProcessorDesktop) imp.start(logonType="NEW_CREDENTIALS") if depot_url.hostname.lower() not in ("127.0.0.1", "localhost", "::1"): logger.notice("Mounting depot share %s", depotRemoteUrl) set_status_message(be, sessionId, _("Mounting depot share %s") % depotRemoteUrl) # pylint: disable=no-member if runAsUser or depot_url.scheme not in ("smb", "cifs"): System.mount(depotRemoteUrl, depotDrive, username=depotServerUsername, password=depotServerPassword) else: System.mount(depotRemoteUrl, depotDrive) depotShareMounted = True logger.notice("Starting action processor") set_status_message(be, sessionId, _("Action processor is running")) # pylint: disable=no-member if imp: imp.runCommand(actionProcessorCommand, timeoutSeconds=actionProcessorTimeout) else: System.execute(actionProcessorCommand, waitForEnding=True, timeout=actionProcessorTimeout) logger.notice("Action processor ended") set_status_message(be, sessionId, _("Action processor ended")) # pylint: disable=no-member except Exception as err: # pylint: disable=broad-except logger.error(err, exc_info=True) error = f"Failed to process action requests: {err}" logger.error(error) if be: set_status_message(be, sessionId, error) if depotShareMounted: try: logger.notice("Unmounting depot share") System.umount(depotDrive) except Exception: # pylint: disable=broad-except pass if imp: try: imp.end() except Exception: # pylint: disable=broad-except pass if be: try: be.backend_exit() except Exception: # pylint: disable=broad-except pass
def showPopup(self, message, mode='prepend', addTimestamp=True, displaySeconds=0): # pylint: disable=too-many-branches,too-many-statements, too-many-locals if mode not in ('prepend', 'append', 'replace'): mode = 'prepend' port = config.get('notification_server', 'popup_port') if not port: raise Exception('notification_server.popup_port not defined') notifierCommand = config.get('opsiclientd_notifier', 'command') if not notifierCommand: raise Exception('opsiclientd_notifier.command not defined') notifierCommand = f'{notifierCommand} -s {os.path.join("notifier", "popup.ini")}' if addTimestamp: message = "=== " + time.strftime( "%Y-%m-%d %H:%M:%S") + " ===\n" + message with self._popupNotificationLock: # pylint: disable=too-many-nested-blocks if (mode in ('prepend', 'append') and self._popupNotificationServer and self._popupNotificationServer.isListening()): # Already runnning try: for subject in self._popupNotificationServer.getSubjects(): if subject.getId() == 'message': if mode == 'prepend': message = message + "\n\n" + subject.getMessage( ) else: message = subject.getMessage( ) + "\n\n" + message break except Exception as err: # pylint: disable=broad-except logger.warning(err, exc_info=True) self.hidePopup() popupSubject = MessageSubject(id='message') choiceSubject = ChoiceSubject(id='choice') popupSubject.setMessage(message) logger.notice( "Starting popup message notification server on port %d", port) try: self._popupNotificationServer = NotificationServer( address="127.0.0.1", start_port=port, subjects=[popupSubject, choiceSubject]) self._popupNotificationServer.daemon = True with log_context({'instance': 'popup notification server'}): if not self._popupNotificationServer.start_and_wait( timeout=30): raise Exception( "Timed out while waiting for notification server") except Exception as err: # pylint: disable=broad-except logger.error("Failed to start notification server: %s", err) raise notifierCommand = notifierCommand.replace( '%port%', str(self._popupNotificationServer.port)).replace( '%id%', "popup") choiceSubject.setChoices([_('Close')]) choiceSubject.setCallbacks([self.popupCloseCallback]) sessionIds = System.getActiveSessionIds() if not sessionIds: sessionIds = [System.getActiveConsoleSessionId()] for sessionId in sessionIds: desktops = [None] if RUNNING_ON_WINDOWS: desktops = ["default", "winlogon"] for desktop in desktops: try: System.runCommandInSession(command=notifierCommand, sessionId=sessionId, desktop=desktop, waitForProcessEnding=False) except Exception as err: # pylint: disable=broad-except logger.error( "Failed to start popup message notifier app in session %s on desktop %s: %s", sessionId, desktop, err) class PopupClosingThread(threading.Thread): def __init__(self, opsiclientd, seconds): super().__init__() self.opsiclientd = opsiclientd self.seconds = seconds self.stopped = False def stop(self): self.stopped = True def run(self): while not self.stopped: time.sleep(1) if time.time() > self.seconds: break if not self.stopped: logger.debug("hiding popup window") self.opsiclientd.hidePopup() # last popup decides end time (even if unlimited) if self._popupClosingThread and self._popupClosingThread.is_alive( ): self._popupClosingThread.stop() if displaySeconds > 0: logger.debug("displaying popup for %s seconds", displaySeconds) self._popupClosingThread = PopupClosingThread( self, time.time() + displaySeconds) self._popupClosingThread.start()
def connectConfigService(self, allowTemporaryConfigServiceUrls=True): # pylint: disable=too-many-locals,too-many-branches,too-many-statements try: # pylint: disable=too-many-nested-blocks configServiceUrls = config.getConfigServiceUrls( allowTemporaryConfigServiceUrls=allowTemporaryConfigServiceUrls ) if not configServiceUrls: raise Exception("No service url defined") if self._loadBalance and (len(configServiceUrls) > 1): random.shuffle(configServiceUrls) for urlIndex, configServiceURL in enumerate(configServiceUrls): self._configServiceUrl = configServiceURL kwargs = self.connectionThreadOptions() logger.debug("Creating ServiceConnectionThread (url: %s)", self._configServiceUrl) serviceConnectionThread = ServiceConnectionThread( configServiceUrl=self._configServiceUrl, username=config.get('global', 'host_id'), password=config.get('global', 'opsi_host_key'), **kwargs) serviceConnectionThread.daemon = True self.connectionStart(self._configServiceUrl) cancellableAfter = forceInt( config.get('config_service', 'user_cancelable_after')) timeout = forceInt( config.get('config_service', 'connection_timeout')) logger.info( "Starting ServiceConnectionThread, timeout is %d seconds", timeout) serviceConnectionThread.start() for _unused in range(5): if serviceConnectionThread.running: break time.sleep(1) logger.debug("ServiceConnectionThread started") while serviceConnectionThread.running and timeout > 0: if self._should_stop: return logger.debug( "Waiting for ServiceConnectionThread (timeout: %d, alive: %s, cancellable in: %d)", timeout, serviceConnectionThread.is_alive(), cancellableAfter) self.connectionTimeoutChanged(timeout) if cancellableAfter > 0: cancellableAfter -= 1 if cancellableAfter == 0: self.connectionCancelable( serviceConnectionThread.stopConnectionCallback) time.sleep(1) timeout -= 1 if serviceConnectionThread.cancelled: self.connectionCanceled() elif serviceConnectionThread.running: serviceConnectionThread.stop() if urlIndex + 1 < len(configServiceUrls): # Try next url continue self.connectionTimedOut() if not serviceConnectionThread.connected: self.connectionFailed( serviceConnectionThread.connectionError) if serviceConnectionThread.connected and ( serviceConnectionThread.getUsername() != config.get( 'global', 'host_id')): config.set('global', 'host_id', serviceConnectionThread.getUsername().lower()) logger.info("Updated host_id to '%s'", config.get('global', 'host_id')) config.updateConfigFile() if serviceConnectionThread.connected and forceBool( config.get('config_service', 'sync_time_from_service')): logger.info("Syncing local system time from service") try: System.setLocalSystemTime( serviceConnectionThread.configService. getServiceTime(utctime=True)) # pylint: disable=no-member except Exception as err: # pylint: disable=broad-except logger.error("Failed to sync time: '%s'", err) if ("localhost" not in configServiceURL and "127.0.0.1" not in configServiceURL): try: config.set( 'depot_server', 'master_depot_id', serviceConnectionThread.configService.getDepotId( config.get('global', 'host_id')) # pylint: disable=no-member ) config.updateConfigFile() except Exception as err: # pylint: disable=broad-except logger.warning(err) self._configService = serviceConnectionThread.configService self.connectionEstablished() except Exception: self.disconnectConfigService() raise
def run(self): # pylint: disable=too-many-locals,too-many-branches,too-many-statements with log_context({'instance': 'service connection'}): logger.debug("ServiceConnectionThread started...") self.running = True self.connected = False self.cancelled = False try: # pylint: disable=too-many-nested-blocks verify_server_cert = ( config.get('global', 'verify_server_cert') or config.get('global', 'verify_server_cert_by_ca')) ca_cert_file = config.ca_cert_file self.prepare_ca_cert_file() compression = config.get('config_service', 'compression') if "localhost" in self._configServiceUrl or "127.0.0.1" in self._configServiceUrl: compression = False verify_server_cert = False if verify_server_cert: if os.path.exists(ca_cert_file): logger.info( "Server verification enabled, using CA cert file '%s'", ca_cert_file) else: logger.error( "Server verification enabled, but CA cert file '%s' not found, skipping verification", ca_cert_file) ca_cert_file = None verify_server_cert = False tryNum = 0 while not self.cancelled and not self.connected: tryNum += 1 try: logger.notice("Connecting to config server '%s' #%d", self._configServiceUrl, tryNum) self.setStatusMessage( _("Connecting to config server '%s' #%d") % (self._configServiceUrl, tryNum)) if len(self._username.split('.')) < 3: raise Exception( f"Domain missing in username '{self._username}'" ) logger.debug( "JSONRPCBackend address=%s, verify_server_cert=%s, ca_cert_file=%s, proxy_url=%s, application=%s", self._configServiceUrl, verify_server_cert, ca_cert_file, config.get('global', 'proxy_url'), f"opsiclientd/{__version__}") self.configService = JSONRPCBackend( address=self._configServiceUrl, username=self._username, password=self._password, verify_server_cert=verify_server_cert, ca_cert_file=ca_cert_file, proxy_url=config.get('global', 'proxy_url'), application=f"opsiclientd/{__version__}", compression=compression, ip_version=config.get('global', 'ip_version')) self.configService.accessControl_authenticated() # pylint: disable=no-member self.connected = True self.connectionError = None serverVersion = self.configService.serverVersion self.setStatusMessage( _("Connected to config server '%s'") % self._configServiceUrl) logger.notice( "Connected to config server '%s' (name=%s, version=%s)", self._configServiceUrl, self.configService.serverName, serverVersion) if serverVersion and (serverVersion[0] > 4 or (serverVersion[0] == 4 and serverVersion[1] > 1)): if not os.path.exists( config.ca_cert_file ) or verify_server_cert or config.get( 'global', 'install_opsi_ca_into_os_store'): # Renew CA if not exists or connection is verified try: update_ca_cert(self.configService, allow_remove=True) except Exception as err: # pylint: disable=broad-except logger.error(err, exc_info=True) except OpsiServiceVerificationError as verificationError: self.connectionError = forceUnicode(verificationError) self.setStatusMessage( _("Failed to connect to config server '%s': Service verification failure" ) % self._configServiceUrl) logger.error( "Failed to connect to config server '%s': %s", self._configServiceUrl, verificationError) break except Exception as error: # pylint: disable=broad-except self.connectionError = forceUnicode(error) self.setStatusMessage( _("Failed to connect to config server '%s': %s") % (self._configServiceUrl, forceUnicode(error))) logger.info( "Failed to connect to config server '%s': %s", self._configServiceUrl, error) logger.debug(error, exc_info=True) if isinstance(error, OpsiAuthenticationError): fqdn = System.getFQDN() try: fqdn = forceFqdn(fqdn) except Exception as fqdnError: # pylint: disable=broad-except logger.warning( "Failed to get fqdn from os, got '%s': %s", fqdn, fqdnError) break if self._username != fqdn: logger.notice( "Connect failed with username '%s', got fqdn '%s' from os, trying fqdn", self._username, fqdn) self._username = fqdn else: break if 'is not supported by the backend' in self.connectionError.lower( ): try: from cryptography.hazmat.backends import default_backend # pylint: disable=import-outside-toplevel logger.debug( "Got the following crypto backends: %s", default_backend()._backends) # pylint: disable=no-member,protected-access except Exception as cryptoCheckError: # pylint: disable=broad-except logger.debug( "Failed to get info about installed crypto modules: %s", cryptoCheckError) for _unused in range( 3): # Sleeping before the next retry time.sleep(1) except Exception as err: # pylint: disable=broad-except logger.error(err, exc_info=True) finally: self.running = False
def __init__(self): baseDir = self._getBaseDirectory() self._temporaryConfigServiceUrls = [] self._temporaryDepotDrive = [] self._temporary_depot_path = None self._config_file_mtime = 0 self.disabledEventTypes = [] self._config = { "system": { "program_files_dir": "", }, "global": { "base_dir": baseDir, "config_file": os.path.join(baseDir, "opsiclientd", "opsiclientd.conf"), "log_file": "opsiclientd.log", "log_level": LOG_NOTICE, "keep_rotated_logs": 10, "max_log_size": 5, # In MB "max_log_transfer_size": 5, # In MB "host_id": System.getFQDN().lower(), "opsi_host_key": "", "wait_for_gui_timeout": 120, "block_login_notifier": "", "verify_server_cert": False, "verify_server_cert_by_ca": False, "trust_uib_opsi_ca": True, "install_opsi_ca_into_os_store": False, "proxy_url": "system", "suspend_bitlocker_on_reboot": False, "ip_version": "auto" }, "config_service": { "url": [], "compression": True, "connection_timeout": 10, "user_cancelable_after": 0, "sync_time_from_service": False }, "depot_server": { # The id of the depot the client is assigned to "master_depot_id": "", # The id of the depot currently set as (dynamic) depot "depot_id": "", "url": "", "drive": "", "username": "******", }, "cache_service": { "product_cache_max_size": 6000000000, "extension_config_dir": "", "include_product_group_ids": [], "exclude_product_group_ids": [] }, "control_server": { "interface": "0.0.0.0", "port": 4441, "ssl_server_key_file": os.path.join(baseDir, "opsiclientd", "opsiclientd.pem"), "ssl_server_cert_file": os.path.join(baseDir, "opsiclientd", "opsiclientd.pem"), "static_dir": os.path.join(baseDir, "opsiclientd", "static_html"), "max_authentication_failures": 5, "kiosk_api_active": True, "process_actions_event": "auto" }, "notification_server": { "interface": "127.0.0.1", "start_port": 44000, "popup_port": 45000, }, "opsiclientd_rpc": { "command": "", }, "opsiclientd_notifier": { "command": "", }, "action_processor": { "local_dir": "", "remote_dir": "", "remote_common_dir": "", "filename": "", "command": "", "run_as_user": "******", "create_user": True, "delete_user": True, "create_environment": False, } } self._applySystemSpecificConfiguration()