Skip to content

Commit 12d206d

Browse files
committed
Add a query_facts() function
1 parent ad70fa9 commit 12d206d

File tree

4 files changed

+195
-3
lines changed

4 files changed

+195
-3
lines changed

docs/examples.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@ Basic query for nodes using :mod:`pypuppetdb`:
55

66
.. literalinclude:: ../examples/nodes.py
77
:lines: 21-
8+
9+
Obtain named facts from nodes matching a query (using :mod:`pypuppetdb`):
10+
11+
.. literalinclude:: ../examples/facts.py
12+
:lines: 21-

examples/facts.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+
Query for nodes using :mod:`pypuppetdb`.
19+
"""
20+
21+
import pypuppetdb
22+
import pypuppetdbquery
23+
24+
pdb = pypuppetdb.connect()
25+
26+
node_facts = pypuppetdbquery.query_facts(
27+
pdb,
28+
'(processorcount=4 or processorcount=8) and kernel=Linux',
29+
['/^lsb/', 'architecture'])
30+
31+
for node in node_facts:
32+
facts = node_facts[node]
33+
print(node, facts)

pypuppetdbquery/__init__.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@
2222
module itself rather than any of the sub-modules.
2323
"""
2424

25+
from collections import defaultdict
2526
from json import dumps as json_dumps
2627
from .evaluator import Evaluator
2728
from .parser import Parser
2829

2930

30-
def parse(s, json=True, lex_options=None, yacc_options=None):
31+
def parse(s, json=True, mode='nodes', lex_options=None, yacc_options=None):
3132
"""
3233
Parse a PuppetDBQuery-style query and transform it into a PuppetDB "AST"
3334
query.
@@ -43,6 +44,7 @@ def parse(s, json=True, lex_options=None, yacc_options=None):
4344
<https://github.com/dalen>`_.
4445
4546
:param str s: The query to parse and transform
47+
:param str mode: The PuppetDB endpoint being queried
4648
:param bool json: Whether to JSON-encode the PuppetDB AST result
4749
:param dict lex_options: Options passed to :func:`ply.lex.lex`
4850
:param dict yacc_options: Options passed to :func:`ply.yacc.yacc`
@@ -51,9 +53,64 @@ def parse(s, json=True, lex_options=None, yacc_options=None):
5153
evaluator = Evaluator()
5254

5355
ast = parser.parse(s)
54-
raw = evaluator.evaluate(ast)
56+
raw = evaluator.evaluate(ast, mode=mode)
5557

5658
if json and raw is not None:
5759
return json_dumps(raw)
5860
else:
5961
return raw
62+
63+
64+
def query_facts(pdb, s, facts=None, raw=False, lex_options=None,
65+
yacc_options=None):
66+
"""
67+
Helper to query PuppetDB for facts on nodes matching a query string.
68+
69+
Adjusts the query to return only those facts requested in the function
70+
call.
71+
72+
The fact names included in `facts` may be names or regular expressions. If
73+
the string starts and ends with ``/``, it is considered a regular
74+
expression (e.g. ``/^lsb/``).
75+
76+
If `raw` is `False` (the default), the return value is a :class:`dict`
77+
with node names as keys containing a :class:`dict` of fact names to fact
78+
values. If `True` it returns raw :class:`pypuppetdb.types.Fact` objects as
79+
:meth:`pypuppetdb.api.BaseAPI.nodes` does.
80+
81+
:param pypuppetdb.api.BaseAPI pdb: pypuppetdb connection to query from
82+
:param str s: The query string (may be empty to query all nodes)
83+
:param Sequence facts: List of fact names to search for
84+
:param bool raw: Whether to skip post-processing the facts into a dict
85+
structure
86+
:param dict lex_options: Options passed to :func:`ply.lex.lex`
87+
:param dict yacc_options: Options passed to :func:`ply.yacc.yacc`
88+
"""
89+
query = parse(s, json=False, mode='facts', lex_options=lex_options,
90+
yacc_options=yacc_options)
91+
92+
if facts:
93+
factquery = ['or']
94+
for fact in facts:
95+
# Regular expression fact name?
96+
if fact[0] == fact[-1] == '/':
97+
factquery.append(['~', 'name', fact[1:-1]])
98+
else:
99+
factquery.append(['=', 'name', fact])
100+
101+
if query:
102+
query = ['and', query, factquery]
103+
else:
104+
query = factquery
105+
106+
if query is None:
107+
return None
108+
109+
facts = pdb.facts(query=json_dumps(query))
110+
if raw:
111+
return facts
112+
113+
ret = defaultdict(dict)
114+
for fact in facts:
115+
ret[fact.node][fact.name] = fact.value
116+
return ret

tests/test_frontend.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,17 @@
1616
# limitations under the License.
1717

1818
import json
19+
import mock
1920
import unittest
2021

21-
from pypuppetdbquery import parse
22+
from pypuppetdbquery import parse, query_facts
23+
24+
25+
class FakeNode(object):
26+
def __init__(self, node, name, value):
27+
self.node = node
28+
self.name = name
29+
self.value = value
2230

2331

2432
class TestFrontend(unittest.TestCase):
@@ -40,6 +48,19 @@ def _parse(self, s, **kwargs):
4048
},
4149
**kwargs)
4250

51+
def _query_facts(self, pdb, s, facts=None, raw=False):
52+
return query_facts(
53+
pdb, s, facts, raw,
54+
lex_options={
55+
'debug': False,
56+
'optimize': False,
57+
},
58+
yacc_options={
59+
'debug': False,
60+
'optimize': False,
61+
'write_tables': False,
62+
})
63+
4364
def test_empty_queries(self):
4465
out = self._parse('')
4566
self.assertTrue(out is None)
@@ -65,3 +86,79 @@ def test_simple_raw(self):
6586
['=', 'path', ['foo']],
6687
['=', 'value', 'bar']]]]]
6788
self.assertEqual(out, expect)
89+
90+
def test_query_facts_with_query_and_facts_list(self):
91+
mock_pdb = mock.NonCallableMock()
92+
mock_pdb.facts = mock.Mock(return_value=[
93+
FakeNode('alpha', 'foo', 'bar'),
94+
])
95+
96+
node_facts = self._query_facts(mock_pdb, 'foo=bar', ['foo'])
97+
98+
mock_pdb.facts.assert_called_once_with(query=json.dumps([
99+
'and',
100+
['in', 'certname',
101+
['extract', 'certname',
102+
['select_fact_contents',
103+
['and',
104+
['=', 'path', ['foo']],
105+
['=', 'value', 'bar']]]]],
106+
['or',
107+
['=', 'name', 'foo']]]))
108+
109+
self.assertEquals(node_facts, {
110+
'alpha': {'foo': 'bar'},
111+
})
112+
113+
def test_query_facts_with_query_and_facts_list_regex(self):
114+
mock_pdb = mock.NonCallableMock()
115+
mock_pdb.facts = mock.Mock(return_value=[
116+
FakeNode('alpha', 'foo', 'bar'),
117+
])
118+
119+
node_facts = self._query_facts(mock_pdb, 'foo=bar', ['/foo/'])
120+
121+
mock_pdb.facts.assert_called_once_with(query=json.dumps([
122+
'and',
123+
['in', 'certname',
124+
['extract', 'certname',
125+
['select_fact_contents',
126+
['and',
127+
['=', 'path', ['foo']],
128+
['=', 'value', 'bar']]]]],
129+
['or',
130+
['~', 'name', 'foo']]]))
131+
132+
self.assertEquals(node_facts, {
133+
'alpha': {'foo': 'bar'},
134+
})
135+
136+
def test_query_facts_with_facts_list_only(self):
137+
mock_pdb = mock.NonCallableMock()
138+
mock_pdb.facts = mock.Mock(return_value=[
139+
FakeNode('alpha', 'foo', 'bar'),
140+
])
141+
142+
node_facts = self._query_facts(mock_pdb, '', ['foo'])
143+
144+
mock_pdb.facts.assert_called_once_with(query=json.dumps([
145+
'or',
146+
['=', 'name', 'foo']]))
147+
148+
self.assertEquals(node_facts, {
149+
'alpha': {'foo': 'bar'},
150+
})
151+
152+
def test_query_facts_without_query_or_facts(self):
153+
node_facts = self._query_facts(None, '')
154+
self.assertTrue(node_facts is None)
155+
156+
def test_query_facts_in_raw_mode(self):
157+
mock_pdb = mock.NonCallableMock()
158+
mock_pdb.facts = mock.Mock(return_value=[
159+
FakeNode('alpha', 'foo', 'bar'),
160+
])
161+
162+
node_facts = self._query_facts(mock_pdb, 'foo=bar', raw=True)
163+
164+
self.assertEquals(node_facts, mock_pdb.facts.return_value)

0 commit comments

Comments
 (0)