def load(self, data, *, many=None, partial=None, unknown=None, **kwargs): # When being passed in via the query string, we may get the raw JSON string instead of # the deserialized dictionary. We need to unpack it ourselves. if isinstance(data, str): try: data = json.loads(data) except json.decoder.JSONDecodeError as exc: raise ValidationError({ "_schema": [ f"Invalid JSON value: '{data}'", str(exc), ], }) elif isinstance(data, QueryExpression): return data if not self.context or "table" not in self.context: raise RuntimeError(f"No table in context for field {self}") if not data: return NothingExpression() try: tree_to_expr(data, self.context["table"]) except ValueError as e: raise ValidationError(str(e)) from e return super().load(data, many=many, partial=partial, unknown=unknown, **kwargs)
def __init__( self, columns: List[Column], filter_expr: QueryExpression = NothingExpression(), ): """A representation of a livestatus query. Args: columns: A list of `Column` instances, these have to be defined as properties on a `Table` class. filter_expr: A filter-expression. These can be created by comparing `Column` instances to something or comparing `LiteralExpression` instances to something. """ self.columns = columns self.column_names = [col.query_name for col in columns] self.filter_expr = filter_expr _tables = {column.table for column in columns} if len(_tables) != 1: raise ValueError( f"Query doesn't specify a single table: {_tables!r}") self.table: Type[Table] = _tables.pop()
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(), )