def _add_scanned_hosts_to_folder(folder: "CREFolder", found: NetworkScanFoundHosts) -> None: network_scan_properties = folder.attribute("network_scan") translation = network_scan_properties.get("translate_names", {}) entries = [] for host_name, ipaddr in found: host_name = translate_hostname(translation, host_name) attrs = update_metadata({}, created_by=_("Network scan")) if "tag_criticality" in network_scan_properties: attrs["tag_criticality"] = network_scan_properties.get( "tag_criticality", "offline") if network_scan_properties.get("set_ipaddress", True): attrs["ipaddress"] = ipaddr if not Host.host_exists(host_name): entries.append((host_name, attrs, None)) with store.lock_checkmk_configuration(): folder.create_hosts(entries) folder.save()
def _wrapper(param: typing.Mapping[str, Any]) -> cmk_http.Response: if not self.skip_locking and self.method != "get": with store.lock_checkmk_configuration(): response = func(param) else: response = func(param) return response
def execute(self, request): # type: (PushSnapshotRequest) -> bool with store.lock_checkmk_configuration(): multitar.extract_from_buffer(request.tar_content, cmk.gui.watolib.activate_changes.get_replication_paths()) try: self._save_site_globals_on_slave_site(request.tar_content) # pending changes are lost cmk.gui.watolib.activate_changes.confirm_all_local_changes() hooks.call("snapshot-pushed") # Create rule making this site only monitor our hosts create_distributed_wato_file(request.site_id, is_slave=True) except Exception: raise MKGeneralException( _("Failed to deploy configuration: \"%s\". " "Please note that the site configuration has been synchronized " "partially.") % traceback.format_exc()) cmk.gui.watolib.changes.log_audit( None, "replication", _("Synchronized with master (my site id is %s.)") % request.site_id) return True
def page_handler() -> None: initialize_wato_html_head() if not config.wato_enabled: raise MKGeneralException( _("WATO is disabled. Please set <tt>wato_enabled = True</tt>" " in your <tt>multisite.mk</tt> if you want to use WATO.")) # config.current_customer can not be checked with CRE repos if cmk_version.is_managed_edition() and not managed.is_provider( config.current_customer): # type: ignore[attr-defined] raise MKGeneralException( _("Check_MK can only be configured on " "the managers central site.")) current_mode = html.request.var("mode") or "main" mode_permissions, mode_class = _get_mode_permission_and_class(current_mode) display_options.load_from_html(html) if display_options.disabled(display_options.N): html.add_body_css_class("inline") # If we do an action, we aquire an exclusive lock on the complete WATO. if html.is_transaction(): with store.lock_checkmk_configuration(): _wato_page_handler(current_mode, mode_permissions, mode_class) else: _wato_page_handler(current_mode, mode_permissions, mode_class)
def _create_snapshots(self): with store.lock_checkmk_configuration(): if not self._changes: raise MKUserError(None, _("Currently there are no changes to activate.")) if self._get_last_change_id() != self._activate_until: raise MKUserError( None, _("Another change has been made in the meantime. Please review it " "to ensure you also want to activate it now and start the " "activation again.")) # Create (legacy) WATO config snapshot start = time.time() logger.debug("Snapshot creation started") # TODO: Remove/Refactor once new changes mechanism has been implemented # This single function is responsible for the slow activate changes (python tar packaging..) snapshot_name = cmk.gui.watolib.snapshots.create_snapshot(self._comment) log_audit(None, "snapshot-created", _("Created snapshot %s") % snapshot_name) work_dir = os.path.join(self.activation_tmp_base_dir, self._activation_id) if cmk_version.is_managed_edition(): import cmk.gui.cme.managed_snapshots as managed_snapshots # pylint: disable=no-name-in-module managed_snapshots.CMESnapshotManager( work_dir, self._get_site_configurations()).generate_snapshots() else: self._generate_snapshots(work_dir) logger.debug("Snapshot creation took %.4f", time.time() - start)
def _process_parent_scan_results( self, task: ParentScanTask, settings: ParentScanSettings, gateways: List ) -> None: gateway = ParentScanResult(*gateways[0][0]) if gateways[0][0] else None state, skipped_gateways, error = gateways[0][1:] if state in ["direct", "root", "gateway"]: # The following code updates the host config. The progress from loading the WATO folder # until it has been saved needs to be locked. with store.lock_checkmk_configuration(): self._configure_host_and_gateway(task, settings, gateway) else: self._logger.error(error) if gateway: self._num_gateways_found += 1 if state in ["direct", "root"]: self._num_directly_reachable_hosts += 1 self._num_unreachable_gateways += skipped_gateways if state == "notfound": self._num_no_gateway_found += 1 if state in ["failed", "dnserror", "garbled"]: self._num_errors += 1
def page(self): # To prevent mixups in written files we use the same lock here as for # the normal WATO page processing. This might not be needed for some # special automation requests, like inventory e.g., but to keep it simple, # we request the lock in all cases. with store.lock_checkmk_configuration(): watolib.init_wato_datastructures(with_wato_lock=False) # TODO: Refactor these two calls to also use the automation_command_registry if self._command == "checkmk-automation": self._execute_cmk_automation() return elif self._command == "push-profile": self._execute_push_profile() return try: automation_command = watolib.automation_command_registry[ self._command] except KeyError: raise MKGeneralException( _("Invalid automation command: %s.") % self._command) self._execute_automation_command(automation_command)
def _do_housekeeping(self): """Cleanup stale activations in case it is needed""" with store.lock_checkmk_configuration(): for activation_id in self._existing_activation_ids(): # skip the current activation_id if self._activation_id == activation_id: continue delete = False manager = ActivateChangesManager() manager.load() try: try: manager.load_activation(activation_id) except RequestTimeout: raise except Exception: # Not existant anymore! delete = True raise delete = not manager.is_running() finally: if delete: shutil.rmtree( "%s/%s" % (ActivateChangesManager.activation_tmp_base_dir, activation_id))
def _wrapper(param): if not self.skip_locking and self.method != "get": with store.lock_checkmk_configuration(): response = func(param) else: response = func(param) return response
def execute(self, request): # type: (PushSnapshotRequest) -> bool with store.lock_checkmk_configuration(): return cmk.gui.watolib.activate_changes.apply_pre_17_sync_snapshot( request.site_id, request.tar_content, Path(cmk.utils.paths.omd_root), cmk.gui.watolib.activate_changes.get_replication_paths())
def _execute_action( api_call: APICallDefinitionDict, request_object: dict[str, Any] ) -> dict[str, Any]: if api_call.get("locking", True): with store.lock_checkmk_configuration(): return _execute_action_no_lock(api_call, request_object) return _execute_action_no_lock(api_call, request_object)
def save_network_scan_result(folder: 'CREFolder', result: NetworkScanResult) -> None: # Reload the folder, lock WATO before to protect against concurrency problems. with store.lock_checkmk_configuration(): # A user might have changed the folder somehow since starting the scan. Load the # folder again to get the current state. write_folder = watolib.Folder.folder(folder.path()) write_folder.set_attribute("network_scan_result", result) write_folder.save()
def page(self): # To prevent mixups in written files we use the same lock here as for # the normal WATO page processing. This might not be needed for some # special automation requests, like inventory e.g., but to keep it simple, # we request the lock in all cases. lock_config = not (self._command == "checkmk-automation" and request.get_str_input_mandatory("automation") == "active-check") with store.lock_checkmk_configuration( ) if lock_config else nullcontext(): self._execute_automation()
def _process_discovery_results(self, task, job_interface, counts, failed_hosts): # The following code updates the host config. The progress from loading the WATO folder # until it has been saved needs to be locked. with store.lock_checkmk_configuration(): Folder.invalidate_caches() folder = Folder.folder(task.folder_path) for hostname in task.host_names: self._process_service_counts_for_host(counts[hostname]) msg = self._process_discovery_result_for_host(folder.host(hostname), failed_hosts.get(hostname, False), counts[hostname]) job_interface.send_progress_update("%s: %s" % (hostname, msg))
def init_wato_datastructures(with_wato_lock=False): if os.path.exists(ConfigDomainCACertificates.trusted_cas_file) and\ not _need_to_create_sample_config(): return def init(): if not os.path.exists(ConfigDomainCACertificates.trusted_cas_file): ConfigDomainCACertificates().activate() _create_sample_config() if with_wato_lock: with store.lock_checkmk_configuration(): init() else: init()
def _process_discovery_results( self, task, job_interface, response: AutomationDiscoveryResponse) -> None: # The following code updates the host config. The progress from loading the WATO folder # until it has been saved needs to be locked. with store.lock_checkmk_configuration(): Folder.invalidate_caches() folder = Folder.folder(task.folder_path) for count, hostname in enumerate(task.host_names, self._num_hosts_processed + 1): self._process_service_counts_for_host( response.results[hostname]) msg = self._process_discovery_result_for_host( folder.host(hostname), response.results[hostname]) job_interface.send_progress_update( f"[{count}/{self._num_hosts_total}] {hostname}: {msg}")
def _do_housekeeping(self): # type: () -> None """Cleanup stale activations in case it is needed""" with store.lock_checkmk_configuration(): for activation_id in self._existing_activation_ids(): self._logger.info("Check activation: %s", activation_id) delete = False manager = ActivateChangesManager() manager.load() # Try to detect whether or not the activation is still in progress. In case the # activation information can not be read, it is likely that the activation has # just finished while we were iterating the activations. # In case loading fails continue with the next activations try: delete = True try: manager.load_activation(activation_id) delete = not manager.is_running() except MKUserError: # "Unknown activation process", is normal after activation -> Delete, but no # error message logging self._logger.debug("Is not running") except Exception as e: self._logger.warning( " Failed to load activation (%s), trying to delete...", e, exc_info=True) self._logger.info(" -> %s", "Delete" if delete else "Keep") if not delete: continue activation_dir = os.path.join( ActivateChangesManager.activation_tmp_base_dir, activation_id) try: shutil.rmtree(activation_dir) except Exception: self._logger.error( " Failed to delete the activation directory '%s'" % activation_dir, exc_info=True)
def _execute_action(api_call, request_object): if api_call.get("locking", True): with store.lock_checkmk_configuration(): return _execute_action_no_lock(api_call, request_object) return _execute_action_no_lock(api_call, request_object)
def _validating_wrapper(param): # TODO: Better error messages, pointing to the location where variables are missing def _format_fields(_messages: Union[List, Dict]) -> str: if isinstance(_messages, list): return ", ".join(_messages) if isinstance(_messages, dict): return ", ".join(_messages.keys()) return "" def _problem(exc_, status_code=400): if isinstance(exc_.messages, dict): messages = exc_.messages else: messages = {"exc": exc_.messages} return problem( status=status_code, title=http.client.responses[status_code], detail= f"These fields have problems: {_format_fields(exc_.messages)}", ext={"fields": messages}, ) if self.method in ("post", "put") and request.get_data(cache=True): try: self._is_expected_content_type(request.content_type) except ValueError as exc: return problem( status=415, detail=str(exc), title="Content type not valid for this endpoint.", ) try: if path_schema: param.update(path_schema().load(param)) except ValidationError as exc: return _problem(exc, status_code=404) try: if query_schema: param.update(query_schema().load( _from_multi_dict(request.args))) if header_schema: param.update(header_schema().load(request.headers)) if request_schema: # Try to decode only when there is data. Decoding an empty string will fail. if request.get_data(cache=True): json_data = request.json or {} else: json_data = {} param["body"] = request_schema().load(json_data) except ValidationError as exc: return _problem(exc, status_code=400) # make pylint happy assert callable(self.func) if self.tag_group == "Setup" and not config.wato_enabled: return problem( status=403, title="Forbidden: WATO is disabled", detail="This endpoint is currently disabled via the " "'Disable remote configuration' option in 'Distributed Monitoring'. " "You may be able to query the central site.", ) if not self.skip_locking and self.method != "get": with store.lock_checkmk_configuration(): response = self.func(param) else: response = self.func(param) response.freeze() if self.output_empty and response.status_code < 400 and response.data: return problem( status=500, title="Unexpected data was sent.", detail=(f"Endpoint {self.operation_id}\n" "This is a bug, please report."), ext={"data_sent": str(response.data)}, ) if self.output_empty: response.content_type = None if response.status_code not in self._expected_status_codes: return problem( status=500, title= f"Unexpected status code returned: {response.status_code}", detail=(f"Endpoint {self.operation_id}\n" "This is a bug, please report."), ext={"codes": self._expected_status_codes}, ) # We assume something has been modified and increase the config generation ID # by one. This is necessary to ensure a warning in the "Activate Changes" GUI # about there being new changes to activate can be given to the user. if request.method != "get" and response.status_code < 300: update_config_generation() # We assume no configuration change on GET and no configuration change on # non-ok responses. if config.wato_use_git: do_git_commit() # We assume something has been modified and increase the config generation ID # by one. This is necessary to ensure a warning in the "Activate Changes" GUI # about there being new changes to activate can be given to the user. if request.method != "get" and response.status_code < 300: update_config_generation() # We assume no configuration change on GET and no configuration change on # non-ok responses. if config.wato_use_git: do_git_commit() # We assume something has been modified and increase the config generation ID # by one. This is necessary to ensure a warning in the "Activate Changes" GUI # about there being new changes to activate can be given to the user. if request.method != "get" and response.status_code < 300: update_config_generation() # We assume no configuration change on GET and no configuration change on # non-ok responses. if config.wato_use_git: do_git_commit() if (self.content_type == "application/json" and response.status_code < 300 and response_schema and response.data): try: data = json.loads(response.data.decode("utf-8")) except json.decoder.JSONDecodeError as exc: return problem( status=500, title="Server was about to send invalid JSON data.", detail="This is an error of the implementation.", ext={ "errors": str(exc), "orig": response.data, }, ) try: outbound = response_schema().dump(data) except ValidationError as exc: return problem( status=500, title="Server was about to send an invalid response.", detail="This is an error of the implementation.", ext={ "errors": exc.messages, "orig": data, }, ) if self.convert_response: response.set_data(json.dumps(outbound)) response.freeze() return response
def _validating_wrapper(param): # TODO: Better error messages, pointing to the location where variables are missing def _format_fields(_messages: Union[List, Dict]) -> str: if isinstance(_messages, list): return ', '.join(_messages) if isinstance(_messages, dict): return ', '.join(_messages.keys()) return '' def _problem(exc_, status_code=400): if isinstance(exc_.messages, dict): messages = exc_.messages else: messages = {'exc': exc_.messages} return problem( status=status_code, title=http.client.responses[status_code], detail= f"These fields have problems: {_format_fields(exc_.messages)}", ext={'fields': messages}, ) if (self.method in ("post", "put") and request.get_data(cache=True) and request.content_type != self.content_type): return problem( status=415, title= f"Content type {request.content_type!r} not supported on this endpoint.", ) try: if path_schema: param.update(path_schema().load(param)) except ValidationError as exc: return _problem(exc, status_code=404) try: if query_schema: param.update(query_schema().load( _from_multi_dict(request.args))) if header_schema: param.update(header_schema().load(request.headers)) if request_schema: # Try to decode only when there is data. Decoding an empty string will fail. if request.get_data(cache=True): json = request.json or {} else: json = {} param['body'] = request_schema().load(json) except ValidationError as exc: return _problem(exc, status_code=400) # make pylint happy assert callable(self.func) # FIXME # We need to get the "original data" somewhere and are currently "piggy-backing" # it on the response instance. This is somewhat problematic because it's not # expected behaviour and not a valid interface of Response. Needs refactoring. if not self.skip_locking and self.method != 'get': with store.lock_checkmk_configuration(): response = self.func(param) else: response = self.func(param) response.freeze() if self.output_empty and response.status_code < 400 and response.data: return problem(status=500, title="Unexpected data was sent.", detail=(f"Endpoint {self.operation_id}\n" "This is a bug, please report."), ext={'data_sent': str(response.data)}) if self.output_empty: response.content_type = None if response.status_code not in self._expected_status_codes: return problem( status=500, title= f"Unexpected status code returned: {response.status_code}", detail=(f"Endpoint {self.operation_id}\n" "This is a bug, please report."), ext={'codes': self._expected_status_codes}) # We assume something has been modified and increase the config generation ID # by one. This is necessary to ensure a warning in the "Activate Changes" GUI # about there being new changes to activate can be given to the user. if request.method != 'get' and response.status_code < 300: update_config_generation() # We assume no configuration change on GET and no configuration change on # non-ok responses. if config.wato_use_git: do_git_commit() if hasattr(response, 'original_data') and response_schema: try: response_schema().load(response.original_data) return response except ValidationError as exc: # Hope we never get here in production. return problem( status=500, title="Server was about to send an invalid response.", detail="This is an error of the implementation.", ext={ 'errors': exc.messages, 'orig': response.original_data }, ) return response