Skip to content

Commit c298d35

Browse files
committed
Add a query_fact_contents() function
1 parent e787457 commit c298d35

File tree

4 files changed

+224
-1
lines changed

4 files changed

+224
-1
lines changed

docs/examples.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@ Obtain named facts from nodes matching a query (using :mod:`pypuppetdb`):
1010

1111
.. literalinclude:: ../examples/facts.py
1212
:lines: 21-
13+
14+
Obtain selected structured fact values from nodes matching a query (using
15+
:mod:`pypuppetdb`):
16+
17+
.. literalinclude:: ../examples/fact_contents.py
18+
:lines: 21-

examples/fact_contents.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# This file is part of pypuppetdbquery.
4+
# Copyright © 2016 Chris Boot <[email protected]>
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
"""
18+
Obtain selected structured fact values from nodes matching a query.
19+
"""
20+
21+
import pypuppetdb
22+
import pypuppetdbquery
23+
24+
pdb = pypuppetdb.connect()
25+
26+
node_facts = pypuppetdbquery.query_fact_contents(
27+
pdb,
28+
'(processorcount=4 or processorcount=8) and kernel=Linux',
29+
['system_uptime.days', 'os.lsb.~"dist.*"'])
30+
31+
for node in node_facts:
32+
facts = node_facts[node]
33+
print(node, facts)

pypuppetdbquery/__init__.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from collections import defaultdict
2626
from json import dumps as json_dumps
27+
from ply.yacc import NullLogger
2728
from .evaluator import Evaluator
2829
from .parser import Parser
2930

@@ -78,6 +79,11 @@ def query_facts(pdb, s, facts=None, raw=False, lex_options=None,
7879
values. If `True` it returns raw :class:`pypuppetdb.types.Fact` objects as
7980
:meth:`pypuppetdb.api.BaseAPI.nodes` does.
8081
82+
.. note:: This function can return only full facts, not elements of
83+
structured facts. For example, only the whole ``os`` fact may be
84+
returned but not the ``os.family`` key within the larger structured
85+
fact. If you need to do this, look at :func:`query_fact_contents`.
86+
8187
:param pypuppetdb.api.BaseAPI pdb: pypuppetdb connection to query from
8288
:param str s: The query string (may be empty to query all nodes)
8389
:param Sequence facts: List of fact names to search for
@@ -114,3 +120,75 @@ def query_facts(pdb, s, facts=None, raw=False, lex_options=None,
114120
for fact in facts:
115121
ret[fact.node][fact.name] = fact.value
116122
return ret
123+
124+
125+
def query_fact_contents(pdb, s, facts=None, raw=False, lex_options=None,
126+
yacc_options=None):
127+
"""
128+
Helper to query PuppetDB for fact contents (i.e. within structured facts)
129+
on nodes matching a query string.
130+
131+
Adjusts the query to return only those strucutred fact keys requested in
132+
the function call.
133+
134+
The facts listed in the `facts` list are run through the query parser and
135+
treated as "identifier paths". This means the same rules apply as for
136+
within the query language, e.g. ``foo.bar`` or ``foo.*`` or even
137+
``foo.~"bar.*"``.
138+
139+
If `raw` is `False` (the default), the return value is a :class:`dict`
140+
with node names as keys containing a :class:`dict` of flattened fact paths
141+
to fact values. If `True` it returns raw query output: a list of
142+
dictionaries (see the `PuppetDB fact-contents documentation
143+
<https://docs.puppet.com/puppetdb/4.1/api/query/v4/fact-contents.html#response-format>`__).
144+
145+
.. note:: This function can only be used to search deeply within structured
146+
facts. It cannot return a whole structured fact, only individual
147+
elements within—but you can return all the elements within a structured
148+
fact if you want by using a regex match.
149+
150+
:param pypuppetdb.api.BaseAPI pdb: pypuppetdb connection to query from
151+
:param str s: The query string (may be empty to query all nodes)
152+
:param Sequence facts: List of fact paths to search for
153+
:param bool raw: Whether to skip post-processing the facts into a dict
154+
structure grouped by node
155+
:param dict lex_options: Options passed to :func:`ply.lex.lex`
156+
:param dict yacc_options: Options passed to :func:`ply.yacc.yacc`
157+
"""
158+
query = parse(s, json=False, mode='facts', lex_options=lex_options,
159+
yacc_options=yacc_options)
160+
161+
if facts:
162+
# We need custom optiosn to start with identifier_path, but that then
163+
# causes warnings to be issued for unreachable symbols so we silence
164+
# those with the NullLogger.
165+
yacc_opt_id = dict(yacc_options) if yacc_options else {}
166+
yacc_opt_id['errorlog'] = NullLogger()
167+
yacc_opt_id['start'] = 'identifier_path'
168+
169+
parser = Parser(lex_options=lex_options, yacc_options=yacc_opt_id)
170+
evaluator = Evaluator()
171+
172+
factquery = ['or']
173+
for fact in facts:
174+
ast = parser.parse(fact)
175+
factquery.append(evaluator.evaluate(ast, mode='facts'))
176+
177+
if query:
178+
query = ['and', query, factquery]
179+
else:
180+
query = factquery
181+
182+
if query is None:
183+
return None
184+
185+
facts = pdb.fact_contents(query=json_dumps(query))
186+
if raw:
187+
return facts
188+
189+
ret = defaultdict(dict)
190+
for fact in facts:
191+
node = fact['certname']
192+
name = '.'.join(fact['path'])
193+
ret[node][name] = fact['value']
194+
return ret

tests/test_frontend.py

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import mock
2020
import unittest
2121

22-
from pypuppetdbquery import parse, query_facts
22+
from pypuppetdbquery import parse, query_facts, query_fact_contents
2323

2424

2525
class _FakeNode(object):
@@ -168,3 +168,109 @@ def test_query_facts_in_raw_mode(self):
168168
node_facts = self._query_facts(mock_pdb, 'foo=bar', raw=True)
169169

170170
self.assertEquals(node_facts, mock_pdb.facts.return_value)
171+
172+
173+
class TestFrontendQueryFactContents(unittest.TestCase):
174+
"""
175+
Test cases targetting :func:`pypuppetdbquery.query_fact_contents`.
176+
"""
177+
178+
def _query_fact_contents(self, pdb, s, facts=None, raw=False):
179+
return query_fact_contents(
180+
pdb, s, facts, raw,
181+
lex_options={
182+
'debug': False,
183+
'optimize': False,
184+
},
185+
yacc_options={
186+
'debug': False,
187+
'optimize': False,
188+
'write_tables': False,
189+
})
190+
191+
def test_with_query_and_facts_list(self):
192+
mock_pdb = mock.NonCallableMock()
193+
mock_pdb.fact_contents = mock.Mock(return_value=[
194+
{
195+
'value': 14,
196+
'certname': 'alpha',
197+
'environment': 'production',
198+
'path': ['system_uptime', 'days'],
199+
'name': 'system_uptime',
200+
},
201+
])
202+
203+
out = self._query_fact_contents(
204+
mock_pdb, 'foo=bar', ['system_uptime.days'])
205+
206+
mock_pdb.fact_contents.assert_called_once_with(query=json.dumps([
207+
'and',
208+
['in', 'certname',
209+
['extract', 'certname',
210+
['select_fact_contents',
211+
['and',
212+
['=', 'path', ['foo']],
213+
['=', 'value', 'bar']]]]],
214+
['or',
215+
['=', 'path',
216+
['system_uptime', 'days']]]]))
217+
218+
self.assertEquals(out, {
219+
'alpha': {'system_uptime.days': 14},
220+
})
221+
222+
def test_without_query(self):
223+
mock_pdb = mock.NonCallableMock()
224+
mock_pdb.fact_contents = mock.Mock(return_value=[
225+
{
226+
'value': 14,
227+
'certname': 'alpha',
228+
'environment': 'production',
229+
'path': ['system_uptime', 'days'],
230+
'name': 'system_uptime',
231+
},
232+
])
233+
234+
out = self._query_fact_contents(mock_pdb, '', ['system_uptime.days'])
235+
236+
mock_pdb.fact_contents.assert_called_once_with(query=json.dumps([
237+
'or',
238+
['=', 'path',
239+
['system_uptime', 'days']]]))
240+
241+
self.assertEquals(out, {
242+
'alpha': {'system_uptime.days': 14},
243+
})
244+
245+
def test_without_either(self):
246+
out = self._query_fact_contents(None, '')
247+
self.assertTrue(out is None)
248+
249+
def test_raw_output(self):
250+
mock_pdb = mock.NonCallableMock()
251+
mock_pdb.fact_contents = mock.Mock(return_value=[
252+
{
253+
'value': 14,
254+
'certname': 'alpha',
255+
'environment': 'production',
256+
'path': ['system_uptime', 'days'],
257+
'name': 'system_uptime',
258+
},
259+
])
260+
261+
out = self._query_fact_contents(
262+
mock_pdb, 'foo=bar', ['system_uptime.days'], True)
263+
264+
mock_pdb.fact_contents.assert_called_once_with(query=json.dumps([
265+
'and',
266+
['in', 'certname',
267+
['extract', 'certname',
268+
['select_fact_contents',
269+
['and',
270+
['=', 'path', ['foo']],
271+
['=', 'value', 'bar']]]]],
272+
['or',
273+
['=', 'path',
274+
['system_uptime', 'days']]]]))
275+
276+
self.assertEquals(out, mock_pdb.fact_contents.return_value)

0 commit comments

Comments
 (0)