import datetime
import json
import logging
from pypuppetdb.errors import APIError
log = logging.getLogger(__name__)
[docs]class BinaryOperator:
"""
This is a parent helper class used to create PuppetDB AST queries
for single key-value pairs for the available operators.
It is possible to directly declare the various types of queries
from this class. For instance the code
BinaryOperator('=', 'certname', 'node1.example.com') generates the
PuppetDB query '["=", "certname", "node1.example.com"]'. It is preferred
to use the child classes as they may have restrictions specific
to that operator.
See
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#binary-operators
for more information.
:param operator: The binary query operation performed. There is
no value checking on this field.
:type operator: :obj:`string`
:param field: The PuppetDB endpoint query field. See endpoint
documentation for valid values.
:type field: any
:param value: The values of the field to match, or not match.
:type value: any
"""
def __init__(self, operator, field, value):
if isinstance(value, datetime.datetime):
value = str(value)
self.data = [operator, field, value]
def __repr__(self):
return f"Query: {self}"
def __str__(self):
return json.dumps(self.json_data())
def json_data(self):
return self.data
[docs]class BooleanOperator:
"""
This is a parent helper class used to create PuppetDB AST queries
for available boolean queries.
It is possible to directly declare a boolean query from this class.
For instance the code BooleanOperator("and") will create an empty
query '["and",]'. An error will be raised if there are no queries
added via :func:`~pypuppetdb.QueryBuilder.BooleanOperator.add`
See
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#binary-operators
for more information.
:param operator: The boolean query operation to perform.
:type operator: :obj:`string`
"""
def __init__(self, operator):
self.operator = operator
self.operations = []
def add(self, query):
if type(query) == list:
for i in query:
self.add(i)
elif type(query) == str:
self.operations.append(json.loads(query))
elif isinstance(query, (BinaryOperator, InOperator, BooleanOperator)):
self.operations.append(query.json_data())
else:
raise APIError(
"Can only accept fixed-string queries, arrays " + "or operator objects"
)
def __repr__(self):
return f"Query: {self}"
def __str__(self):
return json.dumps(self.json_data())
def json_data(self):
if len(self.operations) == 0:
raise APIError("At least one query operation is required")
return [self.operator] + self.operations
[docs]class FunctionOperator:
"""
Performs an aggregate function on the result of a subquery, full
documentation is available at
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#function.
This object can only be used in the field list or group by list of
an ExtractOperator object.
:param function: The name of the function to perform.
:type function: :obj:`str`
:param field: The name of the field to perform the function on. All
functions with the exception of count require this value.
:type field: :obj:`str`
"""
def __init__(self, function, field=None, fmt=None):
if function not in ["count", "avg", "sum", "min", "max", "to_string"]:
raise APIError(f"Unsupport function: {function}")
elif function != "count" and field is None:
raise APIError("Function {} requires a field value".format(function))
elif function == "to_string" and fmt is None:
raise APIError("Function {0} requires an extra 'fmt' parameter")
self.arr = ["function", function]
if field is not None:
self.arr.append(field)
if function == "to_string":
self.arr.append(fmt)
def __repr__(self):
return f"Query: {self}"
def __str__(self):
return json.dumps(self.json_data())
def json_data(self):
return self.arr
[docs]class SubqueryOperator:
"""
Performs a subquery to another puppetDB object, full
documentation is available at
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#subquery-operators
This object must be used in combination with the InOperator according
to documentation.
:param endpoint: The name of the subquery object
:type function: :obj:`str`
"""
def __init__(self, endpoint):
if endpoint not in [
"catalogs",
"edges",
"environments",
"events",
"facts",
"fact_contents",
"fact_paths",
"nodes",
"reports",
"resources",
]:
raise APIError(f"Unsupported endpoint: {endpoint}")
self.query = None
self.arr = [f"select_{endpoint}"]
def add_query(self, query):
if self.query is not None:
raise APIError("Only one query is supported by ExtractOperator")
else:
self.query = True
self.arr.append(query.json_data())
def __repr__(self):
return f"Query: {self}"
def __str__(self):
return json.dumps(self.json_data())
def json_data(self):
return self.arr
[docs]class InOperator:
"""
Performs boolean compare between a field a subquery result
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#subquery-operators
This object must be used in combination with the SubqueryOperator according
to documentation.
:param field: The name of the subquery object
:type function: :obj:`str`
"""
def __init__(self, field):
self.query = None
self.arr = ["in", field]
def add_query(self, query):
if self.query is not None:
raise APIError("Only one query is supported by ExtractOperator")
elif isinstance(query, str):
self.query = True
self.arr.append(json.loads(query))
elif isinstance(query, (ExtractOperator, FromOperator)):
self.query = True
self.arr.append(query.json_data())
else:
raise APIError(
"InOperator.add_query only supports "
"strings, ExtractOperator, and"
"FromOperator objects"
)
def add_array(self, values):
if self.query is not None:
raise APIError("Only one array is supported by the InOperator")
elif isinstance(values, list):
def depth(a_list):
return (isinstance(a_list, list) and len(a_list) != 0) and max(
map(depth, a_list)
) + 1
if depth(values) == 1:
self.query = True
self.arr.append(["array", values])
else:
raise APIError(
"InOperator.add_array: cannot pass in "
"nested arrays (or empty arrays)"
)
else:
raise APIError(
"InOperator.add_array: Ill-formatted array, "
"must be of the format: "
"['array', [<array values>]]"
)
def __repr__(self):
return f"Query: {self}"
def __str__(self):
return json.dumps(self.json_data())
def json_data(self):
return self.arr
[docs]class FromOperator:
"""
From contextual operator that allows for queries on the root endpoint
or subqueries into other entities:
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#from
Ex.)
fr = FromOperator("facts")
fr.add_query(EqualsOperator("foo", "bar"))
fr.add_order_by(["certname"])
fr.add_limit(10)
note: only supports single entity From operations
"""
def __init__(self, endpoint):
valid_entities = [
"aggregate_event_counts",
"catalogs",
"edges",
"environments",
"event_counts",
"events",
"facts",
"fact_contents",
"fact_names",
"fact_paths",
"nodes",
"producers",
"reports",
"resources",
]
if endpoint in valid_entities:
self.endpoint = endpoint
else:
raise APIError(
"Endpoint is invalid. Must be "
"one of the following : %s" % valid_entities
)
self.query = None
self.order_by = []
self.limit = None
self.offset = None
def add_query(self, query):
if self.query is not None:
raise APIError("Only one main query is supported by FromOperator")
elif isinstance(
query,
(
InOperator,
ExtractOperator,
BinaryOperator,
BooleanOperator,
FunctionOperator,
),
):
self.query = query.json_data()
else:
raise APIError("FromOperator.add_field only supports " "Operator Objects")
def add_order_by(self, fields):
def depth(a_list):
return isinstance(a_list, list) and max(map(depth, a_list)) + 1
fields_depth = depth(fields)
if isinstance(fields, list):
if fields_depth == 1 or fields_depth == 2:
self.order_by = fields
else:
raise APIError(
"ExtractOperator.add_order_by only "
"supports lists of fields of depth "
"one or two: [value, <desc/asc>] or "
"[value]"
)
else:
raise APIError(
"ExtractOperator.add_group_by only supports "
"lists of one or more fields"
)
def add_limit(self, lim):
if isinstance(lim, int):
self.limit = lim
else:
raise APIError("ExtractOperator.add_limit only supports ints")
def add_offset(self, off):
if isinstance(off, int):
self.offset = off
else:
raise APIError("ExtractOperator.add_offset only supports ints")
def __repr__(self):
return f"Query: {self}"
def __str__(self):
return json.dumps(self.json_data())
def json_data(self):
if self.query is None:
raise APIError("FromOperator needs one main query")
arr = ["from", self.endpoint, self.query]
if len(self.order_by) > 0:
arr.append(["order_by", self.order_by])
if self.limit is not None:
arr.append(["limit", self.limit])
if self.offset is not None:
arr.append(["offset", self.offset])
return arr
[docs]class EqualsOperator(BinaryOperator):
"""
Builds an equality filter based on the supplied field-value pair as
described
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#equality.
In order to create the following query:
["=", "environment", "production"]
The following code can be used.
EqualsOperator('environment', 'production')
:param field: The PuppetDB endpoint query field. See endpoint
documentation for valid values.
:type field: any
:param value: The value of the field to match, or not match.
:type value: any
"""
def __init__(self, field, value):
super().__init__("=", field, value)
[docs]class GreaterOperator(BinaryOperator):
"""
Builds a greater-than filter based on the supplied field-value pair as
described
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#greater-than.
In order to create the following query:
[">", "catalog_timestamp", "2016-06-01 00:00:00"]
The following code can be used.
GreaterOperator('catalog_timestamp', datetime.datetime(2016, 06, 01))
:param field: The PuppetDB endpoint query field. See endpoint
documentation for valid values.
:type field: any
:param value: Matches if the field is greater than this value.
:type value: Number, timestamp or array
"""
def __init__(self, field, value):
super().__init__(">", field, value)
[docs]class LessOperator(BinaryOperator):
"""
Builds a less-than filter based on the supplied field-value pair as
described
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#less-than.
In order to create the following query:
["<", "catalog_timestamp", "2016-06-01 00:00:00"]
The following code can be used.
LessOperator('catalog_timestamp', datetime.datetime(2016, 06, 01))
:param field: The PuppetDB endpoint query field. See endpoint
documentation for valid values.
:type field: any
:param value: Matches if the field is less than this value.
:type value: Number, timestamp or array
"""
def __init__(self, field, value):
super().__init__("<", field, value)
[docs]class GreaterEqualOperator(BinaryOperator):
"""
Builds a greater-than or equal-to filter based on the supplied
field-value pair as described
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#greater-than-or-equal-to.
In order to create the following query:
[">=", "facts_timestamp", "2016-06-01 00:00:00"]
The following code can be used.
GreaterEqualOperator('facts_timestamp', datetime.datetime(2016, 06, 01))
:param field: The PuppetDB endpoint query field. See endpoint
documentation for valid values.
:type field: any
:param value: Matches if the field is greater than or equal to\
this value.
:type value: Number, timestamp or array
"""
def __init__(self, field, value):
super().__init__(">=", field, value)
[docs]class LessEqualOperator(BinaryOperator):
"""
Builds a less-than or equal-to filter based on the supplied
field-value pair as described
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#less-than-or-equal-to.
In order to create the following query:
["<=", "facts_timestamp", "2016-06-01 00:00:00"]
The following code can be used.
LessEqualOperator('facts_timestamp', datetime.datetime(2016, 06, 01))
:param field: The PuppetDB endpoint query field. See endpoint
documentation for valid values.
:type field: any
:param value: Matches if the field is less than or equal to\
this value.
:type value: Number, timestamp or array
"""
def __init__(self, field, value):
super().__init__("<=", field, value)
[docs]class RegexOperator(BinaryOperator):
"""
Builds a regular expression filter based on the supplied field-value
pair as described
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#regexp-match.
In order to create the following query:
["~", "certname", "www\\d+\\.example\\.com"]
The following code can be used.
RegexOperator('certname', 'www\\d+\\.example\\.com')
:param field: The PuppetDB endpoint query field. See endpoint
documentation for valid values.
:type field: any
:param value: Matches if the field matches this regular expression.
:type value: :obj:`string`
"""
def __init__(self, field, value):
super().__init__("~", field, value)
[docs]class RegexArrayOperator(BinaryOperator):
"""
Builds a regular expression array filter based on the supplied
field-value pair. This query only works on fields with paths as
described
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#regexp-array-match.
In order to create the following query:
["~", "path", ["networking", "eth.*", "macaddress"]]
The following code can be used.
RegexArrayOperator('path', ["networking", "eth.*", "macaddress"])
:param field: The PuppetDB endpoint query field. See endpoint
documentation for valid values.
:type field: any
:param value: Matches if the field matches this regular expression.
:type value: :obj:`list`
"""
def __init__(self, field, value):
super().__init__("~>", field, value)
[docs]class NullOperator(BinaryOperator):
"""
Builds a null filter based on the field and boolean value pair as
described
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#null-is-null.
This filter only works on field that may be null. Value may only
be True or False.
In order to create the following query:
["null?", "deactivated", true]
The following code can be used.
NullOperator('deactivated', True)
:param field: The PuppetDB endpoint query field. See endpoint
documentation for valid values.
:type field: any
:param value: Matches if the field value is null (if True) or\
not null (if False)
:type value: :obj:`bool`
"""
def __init__(self, field, value):
if type(value) != bool:
raise APIError("NullOperator value must be boolean")
super().__init__("null?", field, value)
[docs]class AndOperator(BooleanOperator):
"""
Builds an AND boolean filter. Only results that match ALL
criteria from the included query strings will be returned
from PuppetDB. Full documentation is available
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#and
In order to create the following query:
["and",
["=", "catalog_environment", "production"],
["=", "facts_environment", "production"]]
The following code can be used:
op = AndOperator()
op.add(EqualsOperator("catalog_environment", "production"))
op.add(EqualsOperator("facts_environment", "production"))
"""
def __init__(self):
super().__init__("and")
[docs]class OrOperator(BooleanOperator):
"""
Builds an OR boolean filter. Only results that match ANY
criteria from the included query strings will be returned
from PuppetDB. Full documentation is available
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#or.
In order to create the following query:
["or", ["=", "name", "hostname"], ["=", "name", "architecture"]]
The following code can be used:
op = OrOperator()
op.add(EqualsOperator("name", "hostname"))
op.add(EqualsOperator("name", "architecture"))
"""
def __init__(self):
super().__init__("or")
[docs]class NotOperator(BooleanOperator):
"""
Builds a NOT boolean filter. Only results that DO NOT match
criteria from the included query strings will be returned
from PuppetDB. Full documentation is available
https://puppet.com/docs/puppetdb/7/api/query/v4/ast.html#not
Unlike the other Boolean Operator objects this operator only
accepts a single query string.
In order to create the following query:
["not", ["=", "osfamily", "RedHat"]]
The following code can be used.
op = NotOperator()
op.add(EqualsOperator("osfamily", "RedHat"))
"""
def __init__(self):
super().__init__("not")
def add(self, query):
if len(self.operations) > 0:
raise APIError("This operator only accept one query string")
elif isinstance(query, list) and len(query) > 1:
raise APIError("This operator only accept one query string")
super().add(query)