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['host_name'] if "service_descriptions" not in body: host_expr = And(Downtimes.host_name.op("=", hostname), Downtimes.is_service.op('=', 0)) downtime_commands.delete_downtime_with_query(live, host_expr) else: services_expr = And( Downtimes.host_name.op('=', hostname), Or(*[ Downtimes.service_description == svc_desc for svc_desc in body['service_descriptions'] ])) 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 show_service(params): """Show the monitored service of a host""" service_description = params["service_description"] host_name = params["host_name"] live = sites.live() q = Query( [ Services.description, Services.host_name, Services.state_type, Services.state, Services.last_check, ], filter_expr=And(Services.host_name.op("=", params["host_name"]), Services.description.op("=", service_description)), ) try: service = q.fetchone(live) except ValueError: return problem( status=404, title="The requested service was not found", detail= f"The service description {service_description} did not match any service", ) return constructors.serve_json( constructors.domain_object( domain_type='service', identifier=f"{host_name}-{service_description}", title=f"Service {service_description}", extensions=service, links=[], editable=False, deletable=False))
def show_downtimes(param): """Show all scheduled downtimes""" live = sites.live() sites_to_query = param.get("sites") if sites_to_query: live.only_sites = sites_to_query q = Query( [ Downtimes.id, Downtimes.host_name, Downtimes.service_description, Downtimes.is_service, Downtimes.author, Downtimes.start_time, Downtimes.end_time, Downtimes.recurring, Downtimes.comment, ] ) query_expr = param.get("query") if query_expr is not None: q = q.filter(query_expr) host_name = param.get("host_name") if host_name is not None: q = q.filter(And(Downtimes.host_name.op("=", host_name), Downtimes.is_service.equals(0))) service_description = param.get("service_description") if service_description is not None: q = q.filter(Downtimes.service_description.contains(service_description)) gen_downtimes = q.iterate(live) return _serve_downtimes(gen_downtimes)
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(), )
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 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=400, 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=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=user.ident, comment=comment, ) except ValueError: raise ProblemException( status=400, title="Service group could not be found.", detail=f"Unknown service group: {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=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 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=400, 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=400, 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 tree_to_expr(filter_dict, table: Any = None) -> QueryExpression: """Turn a filter-dict into a QueryExpression. Examples: >>> tree_to_expr({'op': '=', 'left': 'hosts.name', 'right': 'example.com'}) Filter(name = example.com) >>> tree_to_expr({'op': '!=', 'left': 'hosts.name', 'right': 'example.com'}) Filter(name != example.com) >>> tree_to_expr({'op': '!=', 'left': 'name', 'right': 'example.com'}, 'hosts') Filter(name != example.com) >>> tree_to_expr({'op': 'and', \ 'expr': [{'op': '=', 'left': 'hosts.name', 'right': 'example.com'}, \ {'op': '=', 'left': 'hosts.state', 'right': 0}]}) And(Filter(name = example.com), Filter(state = 0)) >>> tree_to_expr({'op': 'or', \ 'expr': [{'op': '=', 'left': 'hosts.name', 'right': 'example.com'}, \ {'op': '=', 'left': 'hosts.name', 'right': 'heute'}]}) Or(Filter(name = example.com), Filter(name = heute)) >>> tree_to_expr({'op': 'not', \ 'expr': {'op': '=', 'left': 'hosts.name', 'right': 'example.com'}}) Not(Filter(name = example.com)) >>> tree_to_expr({'op': 'not', \ 'expr': {'op': 'not', \ 'expr': {'op': '=', \ 'left': 'hosts.name', \ 'right': 'example.com'}}}) Not(Not(Filter(name = example.com))) >>> from cmk.utils.livestatus_helpers.tables import Hosts >>> tree_to_expr({'op': 'not', 'expr': Hosts.name == 'example.com'}) Not(Filter(name = example.com)) >>> tree_to_expr({'op': 'no_way', \ 'expr': {'op': '=', 'left': 'hosts.name', 'right': 'example.com'}}) Traceback (most recent call last): ... ValueError: Unknown operator: no_way Args: filter_dict: A filter-dict, which can either be persisted or passed over the wire. table: Optionally a table name. Only used when the columns are used in plain form (without table name prefixes). Returns: A valid LiveStatus query expression. Raises: ValueError: when unknown columns are queried """ if not isinstance(filter_dict, dict): # FIXME # Because of not having correct Python packages at the root-level, sometimes a # locally defined class ends up having a relative dotted path, like for example # <class 'expressions.BinaryExpression'> # instead of # <class 'cmk.utils.livestatus_helpers.expressions.BinaryExpression'> # While these classes are actually the same, Python treats them distinct, so we can't # just say `isinstance(filter_dict, BinaryExpression)` (or their super-type) here. return cast(QueryExpression, filter_dict) op = filter_dict["op"] if op in LIVESTATUS_OPERATORS: left = filter_dict["left"] if "." in left: _table, column = left.split(".") if table is not None and _table_name(table) != _table: raise ValueError( f"This field can only query table {_table_name(table)!r}. ({left})" ) else: if table is None: raise ValueError("Missing table parameter.") _table = _table_name(table) column = left return BinaryExpression( _lookup_column(_table, column), LiteralExpression(filter_dict["right"]), op, ) if op == "and": return And( *[tree_to_expr(expr, table) for expr in filter_dict["expr"]]) if op == "or": return Or(*[tree_to_expr(expr, table) for expr in filter_dict["expr"]]) if op == "not": return Not(tree_to_expr(filter_dict["expr"], table)) raise ValueError(f"Unknown operator: {op}")