def set_acknowledgement_on_host_service(params): """Acknowledge for services on a host""" host_name = params['host_name'] service_description = unquote(params['service_description']) body = params['body'] service = Query([Services.description, Services.state], And(Services.description.equals(service_description), Services.host_name.equals(host_name))).first(sites.live()) if service is None: return problem( status=404, title=f'Service {service_description!r} on host {host_name!r} does not exist.', detail='It is not currently monitored.', ) if service.state == 0: return problem(status=400, title=f"Service {service_description!r} does not have a problem.", detail="The state is OK.") acknowledge_service_problem( sites.live(), host_name, service_description, sticky=body.get('sticky', False), notify=body.get('notify', False), persistent=body.get('persistent', False), user=_user_id(), comment=body.get('comment', 'Acknowledged'), ) return http.Response(status=204)
def bulk_delete_downtimes(params): """Bulk delete downtimes""" live = sites.live() entries = params['entries'] not_found = [] downtimes: Dict[int, int] = Query( [Downtimes.id, Downtimes.is_service], And(*[Downtimes.id.equals(downtime_id) for downtime_id in entries]), ).to_dict(live) for downtime_id in entries: if downtime_id not in downtimes: not_found.append(downtime_id) if not_found: raise ProblemException(404, http.client.responses[400], f"Downtimes {', '.join(not_found)} not found") for downtime_id, is_service in downtimes.items(): if is_service: downtime_commands.del_service_downtime(live, downtime_id) else: downtime_commands.del_host_downtime(live, downtime_id) return Response(status=204)
def delete_downtime(params): """Delete a scheduled downtime""" body = params['body'] live = sites.live() delete_type = body['delete_type'] if delete_type == "query": downtime_commands.delete_downtime_with_query(live, body['query']) elif delete_type == "by_id": downtime_commands.delete_downtime(live, body['downtime_id']) elif delete_type == "params": hostname = body['hostname'] if "services" not in body: host_expr = Downtimes.host_name.op("~", hostname) downtime_commands.delete_downtime_with_query(live, host_expr) else: services_expr = And(*[ Downtimes.host_name == body['hostname'], Or(*[ Downtimes.service_description == svc_desc for svc_desc in body['services'] ]) ]) downtime_commands.delete_downtime_with_query(live, services_expr) else: return problem( status=400, title="Unhandled delete_type.", detail=f"The downtime-type {delete_type!r} is not supported.") return Response(status=204)
def bulk_set_acknowledgement_on_host_service(params): """Bulk Acknowledge specific services on specific host""" live = sites.live() body = params['body'] host_name = body['host_name'] entries = body.get('entries', []) query = Query([Services.description, Services.state], And( Services.host_name.equals(host_name), Or(*[ Services.description.equals(service_description) for service_description in entries ]))) services = query.to_dict(live) not_found = [] for service_description in entries: if service_description not in services: not_found.append(service_description) if not_found: return problem( status=400, title= f"Services {', '.join(not_found)} not found on host {host_name}", detail='Currently not monitored') up_services = [] for service_description in entries: if services[service_description] == 0: up_services.append(service_description) if up_services: return problem( status=400, title=f"Services {', '.join(up_services)} do not have a problem", detail="The states of these services are OK") for service_description in entries: acknowledge_service_problem( sites.live(), host_name, service_description, sticky=body.get('sticky', False), notify=body.get('notify', False), persistent=body.get('persistent', False), user=str(config.user.id), comment=body.get('comment', 'Acknowledged'), ) return http.Response(status=204)
def bulk_set_acknowledgement_on_hosts(params): """Bulk acknowledge for hosts""" live = sites.live() entries = params['entries'] hosts: Dict[str, int] = { host_name: host_state for host_name, host_state in Query( # pylint: disable=unnecessary-comprehension [Hosts.name, Hosts.state], And(*[Hosts.name.equals(host_name) for host_name in entries]), ).fetch_values(live) } not_found = [] for host_name in entries: if host_name not in hosts: not_found.append(host_name) if not_found: return problem(status=400, title=f"Hosts {', '.join(not_found)} not found", detail='Current not monitored') up_hosts = [] for host_name in entries: if hosts[host_name] == 0: up_hosts.append(host_name) if up_hosts: return problem( status=400, title=f"Hosts {', '.join(up_hosts)} do not have a problem", detail="The states of these hosts are UP") for host_name in entries: acknowledge_host_problem( sites.live(), host_name, sticky=params.get('sticky'), notify=params.get('notify'), persistent=params.get('persistent'), user=_user_id(), comment=params.get('comment', 'Acknowledged'), ) return http.Response(status=204)
def set_acknowledgement_for_service(params): """Acknowledge for a service globally""" service_description = unquote(params['service_description']) body = params['body'] live = sites.live() services = Query( [Services.host_name, Services.description], And( Services.description.equals(service_description), Or( Services.state == 1, Services.state == 2, ), ), ).fetch_values(live) if not len(services): return problem( status=400, title=f'No services {service_description!r} with problems found.', detail='All services are OK.', ) for _host_name, _service_description in services: acknowledge_service_problem( live, _host_name, _service_description, sticky=body.get('sticky', False), notify=body.get('notify', False), persistent=body.get('persistent', False), user=_user_id(), comment=body.get('comment', 'Acknowledged'), ) return http.Response(status=204)
def set_acknowledgement_on_services(params): """Set acknowledgement on related services""" body = params['body'] live = sites.live() sticky = body['sticky'] notify = body['notify'] persistent = body['persistent'] comment = body['comment'] acknowledge_type = body['acknowledge_type'] if acknowledge_type == 'service': description = unquote(body['service_description']) host_name = body['host_name'] service = Query( [Services.host_name, Services.description, Services.state], And(Services.host_name == host_name, Services.description == description)).first(live) if not service: raise ProblemException( status=404, title= f'Service {description!r}@{host_name!r} could not be found.', ) if not service.state: raise ProblemException( status=422, title=f'Service {description!r}@{host_name!r} has no problem.', ) acknowledge_service_problem( live, service.host_name, service.description, sticky=sticky, notify=notify, persistent=persistent, user=config.user.ident, comment=comment, ) elif acknowledge_type == 'servicegroup': service_group = body['servicegroup_name'] try: acknowledge_servicegroup_problem( live, service_group, sticky=sticky, notify=notify, persistent=persistent, user=config.user.ident, comment=comment, ) except ValueError: raise ProblemException( status=404, title="Servicegroup could not be found.", detail=f"Unknown servicegroup: {service_group}", ) elif acknowledge_type == 'service_by_query': services = Query( [Services.host_name, Services.description, Services.state], body['query'], ).fetchall(live) if not services: raise ProblemException( status=422, title='No services with problems found.', detail='All queried services are OK.', ) for service in services: if not service.state: continue acknowledge_service_problem( live, service.host_name, service.description, sticky=sticky, notify=notify, persistent=persistent, user=config.user.ident, comment=comment, ) else: raise ProblemException( status=400, title="Unhandled acknowledge-type.", detail= f"The acknowledge-type {acknowledge_type!r} is not supported.", ) return http.Response(status=204)
def filter(self, filter_expr: QueryExpression) -> 'Query': """Apply additional filters to an existing query. This will return a new `Query` instance. The original one is left untouched.""" return Query(self.columns, And(self.filter_expr, filter_expr))
def from_string( cls, string_query: str, ) -> 'Query': """Constructs a Query instance from a string based LiveStatus-Query Args: string_query: A LiveStatus query as a string. Examples: >>> q = Query.from_string('GET services\\n' ... 'Columns: service_service_description\\n' ... 'Filter: service_service_description = \\n') >>> query_text = ('''GET services ... Columns: host_address host_check_command host_check_type host_custom_variable_names host_custom_variable_values host_downtimes_with_extra_info host_file name host_has_been_checked host_name host_scheduled_downtime_depth host_state service_accept_passive_checks service_acknowledged service_action_url_expanded service_active_checks_enabled service_cache_interval service_cached_at service_check_command service_check_type service_comments_with_extra_info service_custom_variable_names service_custom_variable_values service_custom_variables service_description service_downtimes service_downtimes_with_extra_info service_has_been_checked service_host_name service_icon_image service_in_check_period service_in_notification_period service_in_passive_check_period service_in_service_period service_is_flapping service_last_check service_last_state_change service_modified_attributes_list service_notes_url_expanded service_notifications_enabled service_perf_data service_plugin_output service_pnpgraph_present service_scheduled_downtime_depth service_service_description service_staleness service_state ... Filter: service_state = 0 ... Filter: service_has_been_checked = 1 ... And: 2 ... Negate: ... Filter: service_has_been_checked = 1 ... Filter: service_scheduled_downtime_depth = 0 ... Filter: host_scheduled_downtime_depth = 0 ... And: 2 ... Filter: service_acknowledged = 0 ... Filter: host_state = 1 ... Filter: host_has_been_checked = 1 ... And: 2 ... Negate: ... Filter: host_state = 2 ... Filter: host_has_been_checked = 1 ... And: 2 ... Negate: ... ''') >>> q = Query.from_string(query_text) We can faithfully recreate this query as a dict-representation. >>> q.dict_repr() {'op': 'and', 'expr': [\ {'op': 'not', 'expr': \ {'op': 'and', 'expr': [\ {'op': '=', 'left': 'services.state', 'right': '0'}, \ {'op': '=', 'left': 'services.has_been_checked', 'right': '1'}\ ]}}, \ {'op': '=', 'left': 'services.has_been_checked', 'right': '1'}, \ {'op': 'and', 'expr': [\ {'op': '=', 'left': 'services.scheduled_downtime_depth', 'right': '0'}, \ {'op': '=', 'left': 'services.host_scheduled_downtime_depth', 'right': '0'}\ ]}, \ {'op': '=', 'left': 'services.acknowledged', 'right': '0'}, \ {'op': 'not', 'expr': \ {'op': 'and', 'expr': [\ {'op': '=', 'left': 'services.host_state', 'right': '1'}, \ {'op': '=', 'left': 'services.host_has_been_checked', 'right': '1'}\ ]}}, \ {'op': 'not', 'expr': \ {'op': 'and', 'expr': [\ {'op': '=', 'left': 'services.host_state', 'right': '2'}, \ {'op': '=', 'left': 'services.host_has_been_checked', 'right': '1'}\ ]}}\ ]} >>> q.columns [Column(services.host_address: string), Column(services.host_check_command: string), Column(services.host_check_type: int), Column(services.host_custom_variable_names: list), Column(services.host_custom_variable_values: list), Column(services.host_downtimes_with_extra_info: list), Column(services.host_has_been_checked: int), Column(services.host_name: string), Column(services.host_scheduled_downtime_depth: int), Column(services.host_state: int), Column(services.accept_passive_checks: int), Column(services.acknowledged: int), Column(services.action_url_expanded: string), Column(services.active_checks_enabled: int), Column(services.cache_interval: int), Column(services.cached_at: time), Column(services.check_command: string), Column(services.check_type: int), Column(services.comments_with_extra_info: list), Column(services.custom_variable_names: list), Column(services.custom_variable_values: list), Column(services.custom_variables: dict), Column(services.description: string), Column(services.downtimes: list), Column(services.downtimes_with_extra_info: list), Column(services.has_been_checked: int), Column(services.host_name: string), Column(services.icon_image: string), Column(services.in_check_period: int), Column(services.in_notification_period: int), Column(services.in_passive_check_period: int), Column(services.in_service_period: int), Column(services.is_flapping: int), Column(services.last_check: time), Column(services.last_state_change: time), Column(services.modified_attributes_list: list), Column(services.notes_url_expanded: string), Column(services.notifications_enabled: int), Column(services.perf_data: string), Column(services.plugin_output: string), Column(services.pnpgraph_present: int), Column(services.scheduled_downtime_depth: int), Column(services.description: string), Column(services.staleness: float), Column(services.state: int)] >>> q = Query.from_string('GET hosts\\n' ... 'Columns: name service_description\\n' ... 'Filter: service_description = ') Traceback (most recent call last): ... ValueError: Table 'hosts': Could not decode line 'Filter: service_description = ' All unknown columns are ignored, as there are many places in Checkmk where queries specify wrong or unnecessary columns. Livestatus would normally ignore them. >>> _ = Query.from_string('GET hosts\\n' ... 'Columns: service_service_description\\n' ... 'Filter: service_description = ') Traceback (most recent call last): ... ValueError: Table 'hosts': Could not decode line 'Filter: service_description = ' >>> _ = Query.from_string('GET foobazbar\\n' ... 'Columns: name service_description\\n' ... 'Filter: service_description = ') Traceback (most recent call last): ... ValueError: Table foobazbar was not defined in the tables module. >>> q = Query.from_string('GET hosts\\n' ... 'Columns: name\\n' ... 'Filter: name = heute\\n' ... 'Filter: alias = heute\\n' ... 'Or: 2') >>> q.table.__name__ 'Hosts' >>> q.columns [Column(hosts.name: string)] >>> q.filter_expr Or(Filter(name = heute), Filter(alias = heute)) >>> print(q) GET hosts Columns: name Filter: name = heute Filter: alias = heute Or: 2 So in essence this says that round trips work >>> assert str(q) == str(Query.from_string(str(q))) Returns: A Query instance. Raises: A ValueError if no Query() instance could be created. """ lines = string_query.split("\n") for line in lines: if line.startswith('GET '): parts = line.split() if len(parts) < 2: raise ValueError(f"No table found in line: {line!r}") table_name = parts[1] try: table_class: Type[Table] = getattr(tables, table_name.title()) except AttributeError: raise ValueError( f"Table {table_name} was not defined in the tables module." ) break else: raise ValueError("No table found") for line in lines: if line.startswith('Columns: '): column_names = line.split(": ", 1)[1].lstrip().split() columns: List[Column] = [] for col in column_names: try: columns.append(_get_column(table_class, col)) except AttributeError: pass break else: raise ValueError("No columns found") filters: List[QueryExpression] = [] for line in lines: if line.startswith('Filter: '): try: filters.append(_parse_line(table_class, line)) except AttributeError: raise ValueError( f"Table {table_name!r}: Could not decode line {line!r}" ) elif line.startswith('Or: ') or line.startswith("And: "): op, _count = line.split(": ") count = int(_count) # I'm sorry. :) # We take the last `count` filters and pass them into the BooleanExpression try: expr = { 'or': Or, 'and': And }[op.lower()](*filters[-count:]) except ValueError: raise ValueError(f"Could not parse {op} for {filters!r}") filters = filters[:-count] filters.append(expr) elif line.startswith('Negate:') or line.startswith('Not:'): filters[-1] = Not(filters[-1]) if len(filters) > 1: filters = [And(*filters)] return cls( columns=columns, filter_expr=filters[0] if filters else NothingExpression(), )