facterpy 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
facter/__init__.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import Any, Dict, Generator, Iterator, Optional, Tuple, Union
|
|
6
|
+
|
|
7
|
+
log = logging.getLogger("facter")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _parse_cli_facter_results(
|
|
11
|
+
facter_results: str,
|
|
12
|
+
) -> Generator[Tuple[str, str], None, None]:
|
|
13
|
+
'''Parse key value pairs printed with "=>" separators.
|
|
14
|
+
Used as fallback when JSON output is not available.
|
|
15
|
+
|
|
16
|
+
>>> list(_parse_cli_facter_results("""foo => bar
|
|
17
|
+
... baz => 1
|
|
18
|
+
... foo_bar => True"""))
|
|
19
|
+
[('foo', 'bar'), ('baz', '1'), ('foo_bar', 'True')]
|
|
20
|
+
>>> list(_parse_cli_facter_results("""foo => bar
|
|
21
|
+
... babababababababab
|
|
22
|
+
... baz => 2"""))
|
|
23
|
+
[('foo', 'bar\nbabababababababab'), ('baz', '2')]
|
|
24
|
+
>>> list(_parse_cli_facter_results("""3434"""))
|
|
25
|
+
Traceback (most recent call last):
|
|
26
|
+
...
|
|
27
|
+
ValueError: parse error
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
Uses a generator interface:
|
|
31
|
+
>>> _parse_cli_facter_results("foo => bar").next()
|
|
32
|
+
('foo', 'bar')
|
|
33
|
+
'''
|
|
34
|
+
last_key, last_value = None, []
|
|
35
|
+
for line in filter(None, facter_results.splitlines()):
|
|
36
|
+
res = line.split(" => ", 1)
|
|
37
|
+
if len(res) == 1:
|
|
38
|
+
if not last_key:
|
|
39
|
+
raise ValueError("parse error")
|
|
40
|
+
# Continue multiline value
|
|
41
|
+
last_value.append(res[0]) # type: ignore[unreachable] # mypy 3.8 compat
|
|
42
|
+
else:
|
|
43
|
+
if last_key:
|
|
44
|
+
yield last_key, os.linesep.join(last_value) # type: ignore[unreachable] # mypy 3.8 compat
|
|
45
|
+
last_key, last_value = res[0], [res[1]]
|
|
46
|
+
|
|
47
|
+
# Yield final key-value pair if exists
|
|
48
|
+
if last_key:
|
|
49
|
+
yield last_key, os.linesep.join(last_value)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Facter:
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
facter_path: str = "facter",
|
|
56
|
+
external_dir: Optional[str] = None,
|
|
57
|
+
cache_enabled: bool = True,
|
|
58
|
+
puppet_facts: bool = False,
|
|
59
|
+
legacy_facts: bool = False,
|
|
60
|
+
# Deprecated - kept for backward compatibility
|
|
61
|
+
use_yaml: Optional[bool] = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
self.facter_path = facter_path
|
|
64
|
+
self.external_dir = external_dir
|
|
65
|
+
self.cache_enabled = cache_enabled
|
|
66
|
+
self.puppet_facts = puppet_facts
|
|
67
|
+
self.legacy_facts = legacy_facts
|
|
68
|
+
self._cache: Optional[Dict[str, Any]] = None
|
|
69
|
+
|
|
70
|
+
# Handle deprecated use_yaml parameter
|
|
71
|
+
if use_yaml is not None:
|
|
72
|
+
log.warning(
|
|
73
|
+
"The 'use_yaml' parameter is deprecated. "
|
|
74
|
+
"facterpy now uses JSON by default with text fallback."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def uses_yaml(self) -> bool:
|
|
79
|
+
"""Deprecated property. facterpy now uses JSON by default."""
|
|
80
|
+
log.warning(
|
|
81
|
+
"The 'uses_yaml' property is deprecated. "
|
|
82
|
+
"facterpy now uses JSON by default with text fallback."
|
|
83
|
+
)
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
def run_facter(self, key: Optional[str] = None) -> Union[Dict[str, Any], Any]:
|
|
87
|
+
"""Run the facter executable with an optional specific fact.
|
|
88
|
+
|
|
89
|
+
Uses JSON output by default (facter 3.0+) with fallback to plain text parsing.
|
|
90
|
+
Returns a dictionary if no key is given, and the value if a key is passed.
|
|
91
|
+
|
|
92
|
+
If legacy_facts=True, includes legacy facts (like facter --show-legacy).
|
|
93
|
+
This is required for lookup() to find legacy facts like 'architecture' that
|
|
94
|
+
don't appear in modern structured fact output but work with individual
|
|
95
|
+
facter commands (e.g., 'facter architecture').
|
|
96
|
+
"""
|
|
97
|
+
base_args = [self.facter_path]
|
|
98
|
+
|
|
99
|
+
# Add common arguments
|
|
100
|
+
if self.puppet_facts:
|
|
101
|
+
base_args.append("--puppet")
|
|
102
|
+
if self.external_dir is not None:
|
|
103
|
+
base_args.extend(["--external-dir", self.external_dir])
|
|
104
|
+
if self.legacy_facts:
|
|
105
|
+
base_args.append("--show-legacy")
|
|
106
|
+
if key is not None:
|
|
107
|
+
base_args.append(key)
|
|
108
|
+
|
|
109
|
+
# Try JSON first (preferred)
|
|
110
|
+
json_args = base_args + ["--json"]
|
|
111
|
+
try:
|
|
112
|
+
proc = subprocess.Popen(
|
|
113
|
+
json_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
114
|
+
)
|
|
115
|
+
stdout, stderr = proc.communicate()
|
|
116
|
+
if proc.returncode == 0:
|
|
117
|
+
results = stdout.decode()
|
|
118
|
+
parsed_results = json.loads(results)
|
|
119
|
+
if key is not None:
|
|
120
|
+
return parsed_results.get(key)
|
|
121
|
+
return parsed_results
|
|
122
|
+
except (json.JSONDecodeError, FileNotFoundError, subprocess.SubprocessError):
|
|
123
|
+
# Fall back to text parsing
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
# Fallback to plain text output
|
|
127
|
+
try:
|
|
128
|
+
proc = subprocess.Popen(
|
|
129
|
+
base_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
130
|
+
)
|
|
131
|
+
stdout, stderr = proc.communicate()
|
|
132
|
+
if proc.returncode != 0:
|
|
133
|
+
raise RuntimeError(f"facter command failed: {stderr.decode()}")
|
|
134
|
+
results = stdout.decode()
|
|
135
|
+
if key is not None:
|
|
136
|
+
return results.strip()
|
|
137
|
+
return dict(_parse_cli_facter_results(results))
|
|
138
|
+
except (FileNotFoundError, subprocess.SubprocessError):
|
|
139
|
+
log.exception("Facter execution failed")
|
|
140
|
+
raise
|
|
141
|
+
|
|
142
|
+
def build_cache(self) -> None:
|
|
143
|
+
"""run facter and save the results to `_cache`"""
|
|
144
|
+
cache = self.run_facter()
|
|
145
|
+
self._cache = cache if isinstance(cache, dict) else {}
|
|
146
|
+
|
|
147
|
+
def clear_cache(self) -> None:
|
|
148
|
+
self._cache = None
|
|
149
|
+
|
|
150
|
+
def has_cache(self) -> bool:
|
|
151
|
+
"""Intended to be called before any call that might access the
|
|
152
|
+
cache. If the cache is not selected, then returns False,
|
|
153
|
+
otherwise the cache is build if needed and returns True."""
|
|
154
|
+
if not self.cache_enabled:
|
|
155
|
+
return False
|
|
156
|
+
if self._cache is None:
|
|
157
|
+
self.build_cache()
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
def lookup(self, fact: str, cache: bool = True) -> Any:
|
|
161
|
+
"""Return the value of a given fact and raise a KeyError if
|
|
162
|
+
it is not available. If `cache` is False, force the lookup of
|
|
163
|
+
the fact."""
|
|
164
|
+
if (not cache) or (not self.has_cache()):
|
|
165
|
+
val = self.run_facter(fact)
|
|
166
|
+
if val is None or val == "":
|
|
167
|
+
raise KeyError(fact)
|
|
168
|
+
return val
|
|
169
|
+
if self._cache is None:
|
|
170
|
+
raise RuntimeError("Cache is None but has_cache returned True")
|
|
171
|
+
return self._cache[fact]
|
|
172
|
+
|
|
173
|
+
def get(self, k: str, d: Any = None) -> Any:
|
|
174
|
+
"""Dictionary-like `get` method with a default value"""
|
|
175
|
+
try:
|
|
176
|
+
return self.lookup(k)
|
|
177
|
+
except KeyError:
|
|
178
|
+
return d
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def all(self) -> Dict[str, Any]:
|
|
182
|
+
"""Dictionary representation of all facts"""
|
|
183
|
+
if not self.has_cache():
|
|
184
|
+
result = self.run_facter()
|
|
185
|
+
return result if isinstance(result, dict) else {}
|
|
186
|
+
return self._cache or {}
|
|
187
|
+
|
|
188
|
+
def keys(self) -> Iterator[str]:
|
|
189
|
+
return iter(self.all.keys())
|
|
190
|
+
|
|
191
|
+
def values(self) -> Iterator[Any]:
|
|
192
|
+
return iter(self.all.values())
|
|
193
|
+
|
|
194
|
+
def items(self) -> Iterator[Tuple[str, Any]]:
|
|
195
|
+
return iter(self.all.items())
|
|
196
|
+
|
|
197
|
+
def __getitem__(self, key: str) -> Any:
|
|
198
|
+
return self.lookup(key)
|
|
199
|
+
|
|
200
|
+
def __iter__(self) -> Iterator[str]:
|
|
201
|
+
return self.keys()
|
|
202
|
+
|
|
203
|
+
def __repr__(self) -> str:
|
|
204
|
+
return (
|
|
205
|
+
f"<Facter cache_enabled={self.cache_enabled!r} "
|
|
206
|
+
f"cache_active={self._cache is not None!r}>"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def json(self) -> str:
|
|
210
|
+
"""Return a json dump of all facts"""
|
|
211
|
+
return json.dumps(self.all)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
_FACTER = Facter()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def get_fact(fact: str, default: Any = None) -> Any:
|
|
218
|
+
return _FACTER.get(fact, default)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: facterpy
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python library to provide a cached and dictionary-like interface to Puppet's facter utility
|
|
5
|
+
Author: Kali Norby
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/knorby/facterpy
|
|
8
|
+
Project-URL: Repository, https://github.com/knorby/facterpy
|
|
9
|
+
Project-URL: Issues, https://github.com/knorby/facterpy/issues
|
|
10
|
+
Keywords: facter,puppet,ruby,system,facts
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
+
Classifier: Topic :: System :: Systems Administration
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: Operating System :: POSIX
|
|
16
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: black; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
33
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
34
|
+
Requires-Dist: tox; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
facterpy
|
|
38
|
+
========
|
|
39
|
+
|
|
40
|
+
Python library to provide a cached and dictionary-like interface to [Puppet's facter utility](http://puppetlabs.com/puppet/related-projects/facter).
|
|
41
|
+
|
|
42
|
+
The library uses JSON output by default (facter 3.0+) with automatic fallback to plain text parsing for maximum compatibility and performance.
|
|
43
|
+
|
|
44
|
+
Usage
|
|
45
|
+
-----
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
>>> import facter
|
|
49
|
+
>>> f = facter.Facter()
|
|
50
|
+
>>> f["architecture"]
|
|
51
|
+
'x86_64'
|
|
52
|
+
>>> f.lookup("uptime_seconds")
|
|
53
|
+
195106
|
|
54
|
+
>>> f.lookup("uptime_seconds") # cached result
|
|
55
|
+
195106
|
|
56
|
+
>>> f.lookup("uptime_seconds", cache=False) # force refresh
|
|
57
|
+
195234
|
|
58
|
+
>>> f.get("not_a_fact", "default_value")
|
|
59
|
+
'default_value'
|
|
60
|
+
>>> f.all # get all facts as dictionary
|
|
61
|
+
{'architecture': 'x86_64', 'uptime_seconds': 195234, ...}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Advanced Usage
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# Custom facter path
|
|
68
|
+
f = facter.Facter(facter_path="/usr/local/bin/facter")
|
|
69
|
+
|
|
70
|
+
# External facts directory
|
|
71
|
+
f = facter.Facter(external_dir="/etc/puppetlabs/facter/facts.d")
|
|
72
|
+
|
|
73
|
+
# Include Puppet facts
|
|
74
|
+
f = facter.Facter(puppet_facts=True)
|
|
75
|
+
|
|
76
|
+
# Disable caching
|
|
77
|
+
f = facter.Facter(cache_enabled=False)
|
|
78
|
+
|
|
79
|
+
# Enable legacy facts (equivalent to facter --show-legacy)
|
|
80
|
+
f = facter.Facter(legacy_facts=True)
|
|
81
|
+
f.lookup("architecture") # Works with legacy facts enabled
|
|
82
|
+
f["operatingsystem"] # Legacy facts appear in f.all
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Install
|
|
86
|
+
-------
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install facterpy
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Requirements
|
|
93
|
+
------------
|
|
94
|
+
|
|
95
|
+
**Required:**
|
|
96
|
+
- Python 3.8+
|
|
97
|
+
- `facter` command-line utility (install via system packages or Puppet)
|
|
98
|
+
|
|
99
|
+
**No external Python dependencies** - uses only Python standard library.
|
|
100
|
+
|
|
101
|
+
Compatibility
|
|
102
|
+
-------------
|
|
103
|
+
|
|
104
|
+
- **Python**: 3.8+ (Python 2 support removed in v0.2.0)
|
|
105
|
+
- **Facter**: 3.0+ (JSON output), with fallback support for older versions
|
|
106
|
+
- **Platforms**: Linux, macOS, and other POSIX systems
|
|
107
|
+
|
|
108
|
+
### Legacy Facts
|
|
109
|
+
|
|
110
|
+
Modern facter (4.x+) uses structured facts, so legacy top-level facts like `architecture` are nested under structured facts like `os.architecture`. To access legacy facts that were available in older facter versions:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# Modern behavior (default) - structured facts only
|
|
114
|
+
f = facter.Facter()
|
|
115
|
+
f.lookup("architecture") # Raises KeyError - not in structured output
|
|
116
|
+
f.all["os"]["architecture"] # Works - nested in structured facts
|
|
117
|
+
|
|
118
|
+
# Legacy behavior - includes legacy facts (like facter --show-legacy)
|
|
119
|
+
f = facter.Facter(legacy_facts=True)
|
|
120
|
+
f.lookup("architecture") # Works - legacy fact available
|
|
121
|
+
f["architecture"] # Works - appears in f.all output
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Migration from v0.1.x
|
|
125
|
+
---------------------
|
|
126
|
+
|
|
127
|
+
**Version 1.0.0 represents a major modernization** while maintaining API compatibility. This version bump reflects the significant gap since the last release (12+ years) and commitment to not breaking existing code.
|
|
128
|
+
|
|
129
|
+
- **Breaking changes**: Python 2 support removed, PyYAML dependency removed
|
|
130
|
+
- **Modernization**: Complete rewrite with JSON-first approach, type hints, modern tooling
|
|
131
|
+
- **API stability**: Core API unchanged to preserve compatibility with existing code
|
|
132
|
+
|
|
133
|
+
**Migration notes:**
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# Old (deprecated, shows warning)
|
|
137
|
+
f = facter.Facter(use_yaml=False)
|
|
138
|
+
|
|
139
|
+
# New (recommended)
|
|
140
|
+
f = facter.Facter() # Automatically uses JSON with text fallback
|
|
141
|
+
|
|
142
|
+
# For legacy fact compatibility (if needed)
|
|
143
|
+
f = facter.Facter(legacy_facts=True) # Includes pre-4.x style facts
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Project State
|
|
147
|
+
-------------
|
|
148
|
+
|
|
149
|
+
I wrote this library in 2013 and did very little maintenance since then, despite some apparent usage. The library is simple and focused, which has helped it remain functional. This 1.0.0 modernization brings it up to current standards while preserving the original API. I haven't used Puppet in quite some time, so I'm not an active user of this library, but the comprehensive test suite should help ensure reliability.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
facter/__init__.py,sha256=iFF4EOHKfMkuvLAOm5xGuyIVBBdnRE33Kk3HowN_iRo,7556
|
|
2
|
+
facterpy-1.0.0.dist-info/licenses/LICENSE,sha256=goHNs7xDDii7xgwaX1TPhEAKGIb7KdvkouwGQlF7Lsc,1283
|
|
3
|
+
facterpy-1.0.0.dist-info/METADATA,sha256=2ZfN9GguL5GBk2oxxrp8DQ3W5G8T6BYnlpCG0eu-Fn8,5139
|
|
4
|
+
facterpy-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
facterpy-1.0.0.dist-info/top_level.txt,sha256=-x6G-A1aImWfM5qqTno8sLrEugOXqvSmXbOrq7bgfwk,7
|
|
6
|
+
facterpy-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Copyright (c) 2013, Kali Norby
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are
|
|
6
|
+
met:
|
|
7
|
+
|
|
8
|
+
Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
this list of conditions and the following disclaimer. Redistributions
|
|
10
|
+
in binary form must reproduce the above copyright notice, this list of
|
|
11
|
+
conditions and the following disclaimer in the documentation and/or
|
|
12
|
+
other materials provided with the distribution. THIS SOFTWARE IS
|
|
13
|
+
PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
|
14
|
+
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
15
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
16
|
+
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
17
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
18
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
19
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
20
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
21
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
22
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
23
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
facter
|