def check_restart_marker(self): logger.info("Checking if restart marker '%s' exists", self.restart_marker) if os.path.exists(self.restart_marker): if os.path.getsize(self.restart_marker) == 0: logger.notice( "Old restart marker found, gui startup and daemon startup events disabled" ) self.disabledEventTypes = ["gui startup", "daemon startup"] else: logger.notice("Reading restart marker") with open(self.restart_marker, "r", encoding="utf-8") as file: for line in file.readlines(): line = line.strip() if line.startswith("#") or not "=" in line: continue option, value = line.split("=", 1) option = option.strip().lower() if option == "disabled_event_types": self.disabledEventTypes = [ v.strip().lower() for v in value.split(",") if v.strip().lower() ] logger.notice( "Event types %s disabled by restart marker", self.disabledEventTypes) try: os.remove(self.restart_marker) except Exception as err: # pylint: disable=broad-except logger.error(err)
def reconfigureEventGenerators(): eventConfigs = getEventConfigs() for eventGenerator in _EVENT_GENERATORS.values(): eventGenerator.setEventConfigs([]) for (eventConfigId, eventConfig) in eventConfigs.items(): mainEventConfigId = eventConfigId.split('{')[0] try: eventGenerator = _EVENT_GENERATORS[mainEventConfigId] except KeyError: logger.info( "Cannot reconfigure event generator for event '%s': not found", eventConfigId) continue try: eventType = eventConfig['type'] del eventConfig['type'] ec = EventConfigFactory(eventType, eventConfigId, **eventConfig) eventGenerator.addEventConfig(ec) logger.notice("Event config '%s' added to event generator '%s'", eventConfigId, mainEventConfigId) except Exception as err: # pylint: disable=broad-except logger.error("Failed to reconfigure event generator '%s': %s", mainEventConfigId, err)
def connectionTimedOut(self): error = ( f"Failed to connect to config service '{self._configServiceUrl}': " f"timed out after {config.get('config_service', 'connection_timeout')} seconds" ) logger.error(error) raise Exception(error)
def run(self): with log_context({'instance': 'control pipe'}): try: while not self._stopEvent.is_set(): if self.clientInfo: self.checkConnection() else: # Old protocol with self.comLock: request = self.read() if request: logger.info("Received request '%s' from %s", request, self) response = self.processIncomingRpc(request) logger.info("Sending response '%s' to %s", response, self) self.write(response) if self.clientInfo: # Switch to new protocol self.executeRpc('blockLogin', [ self._controller._opsiclientd. _blockLogin ], with_lock=False) # pylint: disable=protected-access time.sleep(0.5) except Exception as err: # pylint: disable=broad-except logger.error(err, exc_info=True) finally: self.clientDisconnected()
def getFromService(self, configService): ''' Get settings from service ''' logger.notice("Getting config from service") if not configService: raise Exception("Config service is undefined") query = { "objectId": self.get('global', 'host_id'), "configId": [ 'clientconfig.configserver.url', 'clientconfig.depot.drive', 'clientconfig.depot.id', 'clientconfig.depot.user', 'clientconfig.suspend_bitlocker_on_reboot', 'opsiclientd.*' # everything starting with opsiclientd. ] } configService.backend_setOptions({"addConfigStateDefaults": True}) for configState in configService.configState_getObjects(**query): logger.info("Got config state from service: %r", configState) if not configState.values: logger.debug("No values - skipping %s", configState.configId) continue if configState.configId == 'clientconfig.configserver.url': self.set('config_service', 'url', configState.values) elif configState.configId == 'clientconfig.depot.drive': self.set('depot_server', 'drive', configState.values[0]) elif configState.configId == 'clientconfig.depot.id': self.set('depot_server', 'depot_id', configState.values[0]) elif configState.configId == 'clientconfig.depot.user': self.set('depot_server', 'username', configState.values[0]) elif configState.configId == 'clientconfig.suspend_bitlocker_on_reboot': self.set('global', 'suspend_bitlocker_on_reboot', configState.values[0]) elif configState.configId.startswith('opsiclientd.'): try: parts = configState.configId.lower().split('.') if len(parts) < 3: logger.debug( "Expected at least 3 parts in %s - skipping.", configState.configId) continue value = configState.values if len(value) == 1: value = value[0] self.set(section=parts[1], option=parts[2], value=value) except Exception as err: # pylint: disable=broad-except logger.error("Failed to process configState '%s': %s", configState.configId, err) logger.notice("Got config from service") logger.debug("Config is now:\n %s", objectToBeautifiedText(self.getDict()))
def clearRebootRequest(self): rebootFile = os.path.join(self._PID_DIR, "reboot") if os.path.exists(rebootFile): try: os.remove(rebootFile) except OSError as err: logger.error("Failed to remove reboot file %s: %s", err, rebootFile)
def clearShutdownRequest(self): shutdownFile = os.path.join(self._PID_DIR, "shutdown") if os.path.exists(shutdownFile): try: os.remove(shutdownFile) except OSError as err: logger.error("Failed to remove shutdown file %s: %s", err, shutdownFile)
def disconnectConfigService(self): if self._configService: try: self._configService.backend_exit() except Exception as exit_error: # pylint: disable=broad-except logger.error("Failed to disconnect config service: %s", exit_error) self._configService = None
def hidePopup(self): if self._popupNotificationServer: try: logger.info("Stopping popup message notification server") self._popupNotificationServer.stop(stopReactor=False) except Exception as err: # pylint: disable=broad-except logger.error("Failed to stop popup notification server: %s", err)
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 self_update_from_file(self, filename): logger.notice("Self-update from file %s", filename) test_file = "base_library.zip" inst_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) if not os.path.exists(os.path.join(inst_dir, test_file)): raise RuntimeError( f"File not found: {os.path.join(inst_dir, test_file)}") if self._selfUpdating: raise RuntimeError("Self-update already running") self._selfUpdating = True try: with tempfile.TemporaryDirectory() as tmpdir: destination = os.path.join(tmpdir, "content") shutil.unpack_archive(filename=filename, extract_dir=destination) bin_dir = destination if not os.path.exists(os.path.join(bin_dir, test_file)): bin_dir = None for fn in os.listdir(destination): if os.path.exists( os.path.join(destination, fn, test_file)): bin_dir = os.path.join(destination, fn) break if not bin_dir: raise RuntimeError("Invalid archive") try: check_signature(bin_dir) except Exception as err: # pylint: disable=broad-except logger.error("Could not verify signature!\n%s", err, exc_info=True) logger.error("Not performing self_update.") raise RuntimeError("Invalid signature") from err binary = os.path.join(bin_dir, os.path.basename(self._argv[0])) logger.info("Testing new binary: %s", binary) out = subprocess.check_output([binary, "--version"]) logger.info(out) move_dir = inst_dir + "_old" logger.info("Moving current installation dir '%s' to '%s'", inst_dir, move_dir) if os.path.exists(move_dir): shutil.rmtree(move_dir) os.rename(inst_dir, move_dir) logger.info("Installing '%s' into '%s'", bin_dir, inst_dir) shutil.copytree(bin_dir, inst_dir) self.restart(3) finally: self._selfUpdating = False
def start(self): db_file = config.get('global', 'timeline_db') logger.notice("Starting timeline (database location: %s)", db_file) try: self._createDatabase() except sqlite3.DatabaseError as err: logger.error( "Failed to connect to database %s: %s, recreating database", db_file, err) self._createDatabase(delete_existing=True) self._cleanupDatabase()
def _readStateFile(self): with self._stateLock: try: if os.path.exists(self._stateFile): with codecs.open(self._stateFile, 'r', 'utf8') as stateFile: jsonstr = stateFile.read() self._state = json.loads(jsonstr) except Exception as error: # pylint: disable=broad-except logger.error("Failed to read state file '%s': %s", self._stateFile, error)
def _cleanupDatabase(self): with self._db_lock, self._sql.session() as session: try: self._sql.execute( session, f'delete from EVENT where `start` < "{timestamp(time.time() - 7*24*3600)}"' ) self._sql.update(session, 'EVENT', '`durationEvent` = 1 AND `end` is NULL', {'durationEvent': False}) except Exception as cleanup_error: # pylint: disable=broad-except logger.error(cleanup_error)
def _writeStateFile(self): with self._stateLock: try: jsonstr = json.dumps(self._state) if not os.path.exists(os.path.dirname(self._stateFile)): os.makedirs(os.path.dirname(self._stateFile)) with codecs.open(self._stateFile, 'w', 'utf8') as stateFile: stateFile.write(jsonstr) except Exception as error: # pylint: disable=broad-except logger.error("Failed to write state file '%s': %s", self._stateFile, error)
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 __init__(self, args): """ Initialize service and create stop event """ self.opsiclientd = None try: logger.debug("OpsiclientdService initiating") win32serviceutil.ServiceFramework.__init__(self, args) self._stopEvent = win32event.CreateEvent(None, 0, 0, None) socket.setdefaulttimeout(60) logger.debug("OpsiclientdService initiated") except Exception as err: # pylint: disable=broad-except logger.error(err, exc_info=True) raise
def setup(full=False, options=None) -> None: logger.notice("Running opsiclientd setup") errors = [] if full: opsi_service_setup(options) try: install_service() except Exception as err: # pylint: disable=broad-except logger.error("Failed to install service: %s", err, exc_info=True) errors.append(str(err)) try: setup_ssl(full) except Exception as err: # pylint: disable=broad-except logger.error("Failed to setup ssl: %s", err, exc_info=True) errors.append(str(err)) try: setup_firewall() except Exception as err: # pylint: disable=broad-except logger.error("Failed to setup firewall: %s", err, exc_info=True) errors.append(str(err)) try: setup_on_shutdown() except Exception as err: # pylint: disable=broad-except logger.error("Failed to setup on_shutdown: %s", err, exc_info=True) errors.append(str(err)) logger.notice("Setup completed with %d errors", len(errors)) if errors and full: raise RuntimeError(", ".join(errors))
def run(self): with opsicommon.logging.log_context( {'instance': f'event generator {self._generatorConfig.getId()}'}): try: logger.info("Initializing event generator '%s'", self) self.initialize() if self._generatorConfig.activationDelay > 0: logger.debug( "Waiting %d seconds before activation of event generator '%s'", self._generatorConfig.activationDelay, self) time.sleep(self._generatorConfig.activationDelay) logger.info("Activating event generator '%s'", self) while not self._stopped and ( (self._generatorConfig.maxRepetitions < 0) or (self._eventsOccured <= self._generatorConfig.maxRepetitions)): logger.info("Getting next event...") event = self.getNextEvent() # pylint: disable=assignment-from-none,assignment-from-no-return self._eventsOccured += 1 # Count as occured, even if event is None! if event: logger.info("Got new event: %s (%d/%d)", event, self._eventsOccured, self._generatorConfig.maxRepetitions + 1) self.fireEvent(event) for _unused in range(10): if self._stopped: break time.sleep(1) if not self._stopped: logger.notice( "Event generator '%s' now deactivated after %d event occurrences", self, self._eventsOccured) except Exception as err: # pylint: disable=broad-except if not self._stopped: logger.error("Failure in event generator '%s': %s", self, err, exc_info=True) try: self.cleanup() except Exception as err: # pylint: disable=broad-except if not self._stopped: logger.error("Failed to clean up: %s", err) logger.info("Event generator '%s' exiting ", self)
def opsi_service_setup(options=None): try: config.readConfigFile() except Exception as err: # pylint: disable=broad-except logger.info(err) if os.path.exists(config.ca_cert_file): # Delete ca cert which could be invalid or expired os.remove(config.ca_cert_file) service_address = getattr(options, "service_address", None) or config.get( 'config_service', 'url')[0] service_username = getattr(options, "service_username", None) or config.get('global', 'host_id') service_password = getattr(options, "service_password", None) or config.get('global', 'opsi_host_key') if getattr(options, "client_id", None): config.set('global', 'host_id', options.client_id) if not config.get('global', 'host_id'): fqdn = get_fqdn() fqdn = config.set('global', 'host_id', fqdn) secret_filter.add_secrets(service_password) logger.notice("Connecting to '%s' as '%s'", service_address, service_username) jsonrpc_client = JSONRPCClient(address=service_address, username=service_username, password=service_password, verify_server_cert=False) try: update_ca_cert(jsonrpc_client, allow_remove=False) except Exception as err: # pylint: disable=broad-except logger.error(err, exc_info=True) try: client = jsonrpc_client.host_getObjects(id=config.get( 'global', 'host_id')) # pylint: disable=no-member if client and client[0] and client[0].opsiHostKey: config.set('global', 'opsi_host_key', client[0].opsiHostKey) config.getFromService(jsonrpc_client) config.updateConfigFile(force=True) finally: jsonrpc_client.disconnect()
def run(self): with opsicommon.logging.log_context({ 'instance': 'event generator ' + self._event.eventConfig.getId() }): if self._event.eventConfig.notificationDelay > 0: logger.debug( "Waiting %d seconds before notifying listener '%s' of event '%s'", self._event.eventConfig.notificationDelay, self._eventListener, self._event) time.sleep(self._event.eventConfig.notificationDelay) try: logger.info("Calling processEvent on listener %s", self._eventListener) self._eventListener.processEvent(self._event) except Exception as err: # pylint: disable=broad-except logger.error(err, exc_info=True)
def ReportServiceStatus(self, serviceStatus, waitHint=5000, win32ExitCode=0, svcExitCode=0): # pylint: disable=invalid-name # Wrapping because ReportServiceStatus sometimes lets windows # report a crash of opsiclientd (python 2.6.5) invalid handle try: logger.debug("Reporting service status: %s", serviceStatus) win32serviceutil.ServiceFramework.ReportServiceStatus( self, serviceStatus, waitHint=waitHint, win32ExitCode=win32ExitCode, svcExitCode=svcExitCode) except Exception as err: # pylint: disable=broad-except logger.error("Failed to report service status %s: %s", serviceStatus, err)
def addEvent(self, title, description='', isError=False, category=None, durationEvent=False, start=None, end=None): # pylint: disable=too-many-arguments if self._stopped: return -1 with self._db_lock, self._sql.session() as session: try: if category: category = forceUnicode(category) if not start: start = timestamp() start = forceOpsiTimestamp(start) if end: end = forceOpsiTimestamp(end) durationEvent = True event = { 'title': forceUnicode(title), 'category': category, 'description': forceUnicode(description), 'isError': forceBool(isError), 'durationEvent': forceBool(durationEvent), 'start': start, 'end': end, } try: return self._sql.insert(session, 'EVENT', event) except sqlite3.DatabaseError as db_error: logger.error( "Failed to add event '%s': %s, recreating database", title, db_error) self._sql.delete_db() self._createDatabase(delete_existing=True) return self._sql.insert(session, 'EVENT', event) except Exception as add_error: # pylint: disable=broad-except logger.error("Failed to add event '%s': %s", title, add_error) return -1
def setEventEnd(self, eventId, end=None): if self._stopped: return -1 with self._db_lock, self._sql.session() as session: try: eventId = forceInt(eventId) if not end: end = timestamp() end = forceOpsiTimestamp(end) return self._sql.update(session, 'EVENT', f'`id` = {eventId}', { 'end': end, 'durationEvent': True }) except Exception as end_error: # pylint: disable=broad-except logger.error("Failed to set end of event '%s': %s", eventId, end_error) return -1
def executeRpc(self, method, params=None, with_lock=True): params = params or [] with log_context({'instance': 'control pipe'}): rpc_id = 1 if not self.clientInfo: return { "id": rpc_id, "error": f"Cannot execute rpc, not supported by client {self}", "result": None } request = {"id": rpc_id, "method": method, "params": params} try: if with_lock: self.comLock.acquire() # pylint: disable=consider-using-with try: request_json = toJson(request) logger.info("Sending request '%s' to client %s", request_json, self) self.write(request_json) response_json = self.read() if not response_json: logger.warning( "No response for method '%s' received from client %s", request["method"], self) return {"id": rpc_id, "error": None, "result": None} logger.info("Received response '%s' from client %s", response_json, self) response = fromJson(response_json) if method == "loginUser" and response.get("result"): # Credential provider can only handle one successful login. # Ensure, that the credential provider is not used for a # second login if it keeps the pipe connection open. self.login_capable = False return response finally: if with_lock: self.comLock.release() except Exception as client_err: # pylint: disable=broad-except logger.error(client_err, exc_info=True) return {"id": rpc_id, "error": str(client_err), "result": None}
def getDaemonLoopingContext(): with getEventGeneratorContext(): for event_generator in getEventGenerators( generatorClass=DaemonStartupEventGenerator): try: event_generator.createAndFireEvent() except ValueError as err: logger.error( "Unable to fire DaemonStartupEvent from %s: %s", event_generator, err, exc_info=True) if getEventGenerators(generatorClass=GUIStartupEventGenerator): # Wait until gui starts up logger.notice( "Waiting for gui startup (timeout: %d seconds)", config.get('global', 'wait_for_gui_timeout')) self.waitForGUI( timeout=config.get('global', 'wait_for_gui_timeout')) if not self.is_stopping(): logger.notice("Done waiting for GUI") # Wait some more seconds for events to fire time.sleep(5) try: yield finally: for event_generator in getEventGenerators( generatorClass=DaemonShutdownEventGenerator): logger.info( "Create and fire shutdown event generator %s", event_generator) try: event_generator.createAndFireEvent() except ValueError as err: logger.error( "Unable to fire DaemonStartupEvent from %s: %s", event_generator, err, exc_info=True)
def start_pty(shell="powershell.exe", lines=30, columns=120): logger.notice("Starting %s (%d/%d)", shell, lines, columns) try: # Import of winpty may sometimes fail because of problems with the needed dll. # Therefore we do not import at toplevel from winpty import PtyProcess # pylint: disable=import-error,import-outside-toplevel except ImportError as err: logger.error("Failed to start pty: %s", err, exc_info=True) raise process = PtyProcess.spawn(shell, dimensions=(lines, columns)) def read(length: int): return process.read(length).encode("utf-8") def write(data: bytes): return process.write(data.decode("utf-8")) def stop(): process.close() return (read, write, stop)
def getControlPipe(): logger.notice("Starting control pipe") try: self._controlPipe = ControlPipeFactory(self) self._controlPipe.daemon = True self._controlPipe.start() logger.notice("Control pipe started") yield except Exception as err: # pylint: disable=broad-except logger.error("Failed to start control pipe: %s", err, exc_info=True) raise finally: logger.info("Stopping control pipe") try: self._controlPipe.stop() self._controlPipe.join(2) logger.info("Control pipe stopped") except (NameError, RuntimeError) as stopError: logger.debug("Stopping controlPipe failed: %s", stopError)
def readConfigFile(self): ''' Get settings from config file ''' logger.notice("Trying to read config from file: '%s'", self.get('global', 'config_file')) try: self._config_file_mtime = os.path.getmtime( self.get('global', 'config_file')) # Read Config-File config = IniFile(filename=self.get('global', 'config_file'), raw=True).parse() # Read log settings early if config.has_section('global') and config.has_option( 'global', 'log_level'): self.set('global', 'log_level', config.get('global', 'log_level')) for section in config.sections(): logger.debug("Processing section '%s' in config file: '%s'", section, self.get('global', 'config_file')) for (option, value) in config.items(section): option = option.lower() self.set(section.lower(), option, value) except Exception as err: # pylint: disable=broad-except # An error occured while trying to read the config file logger.error("Failed to read config file '%s': %s", self.get('global', 'config_file'), err) logger.error(err, exc_info=True) return if not self.get("depot_server", "master_depot_id"): self.set("depot_server", "master_depot_id", self.get("depot_server", "depot_id")) logger.notice("Config read") logger.debug("Config is now:\n %s", objectToBeautifiedText(self._config))
def init_logging(log_dir: str, stderr_level: int = LOG_NONE, log_filter: str = None): if not os.path.isdir(log_dir): log_dir = tempfile.gettempdir() log_file = os.path.join(log_dir, "opsiclientd.log") config.set("global", "log_file", log_file) log_file_without_ext, ext = os.path.splitext(log_file) # ext contains '.' for i in (9, 8, 7, 6, 5, 4, 3, 2, 1, 0): old_lf = f"{log_file_without_ext}{ext}.{i-1}" # old format new_lf = f"{log_file_without_ext}_{i}{ext}" if i > 0 and os.path.exists(old_lf): try: # Rename existing log file from old to new format os.rename(old_lf, new_lf) except Exception as err: # pylint: disable=broad-except logger.error("Failed to rename %s to %s: %s", old_lf, new_lf, err) logging_config( stderr_level=stderr_level, stderr_format=DEFAULT_STDERR_LOG_FORMAT, log_file=log_file, file_level=LOG_DEBUG, file_format=DEFAULT_FILE_LOG_FORMAT, file_rotate_max_bytes=config.get("global", "max_log_size")*1000*1000, file_rotate_backup_count=config.get("global", "keep_rotated_logs") ) def namer(default_name): tmp = default_name.rsplit(".", 2) return f"{tmp[0]}_{int(tmp[2]) - 1}.{tmp[1]}" handler = get_all_handlers(handler_type=RotatingFileHandler)[0] handler.namer = namer handler.doRollover() if log_filter: set_filter_from_string(log_filter) logger.essential("Log file %s started", log_file)