Skip to content

Commit 0bb1fb9

Browse files
committed
facts: add server.Group based on getent and tests
1 parent f0a2ef6 commit 0bb1fb9

File tree

4 files changed

+109
-3
lines changed

4 files changed

+109
-3
lines changed

pyinfra/facts/server.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import shutil
66
from datetime import datetime
77
from tempfile import mkdtemp
8-
from typing import Dict, List, Optional
8+
from typing import Dict, Iterable, List, Optional, Union
99

1010
from dateutil.parser import parse as parse_date
1111
from distro import distro
@@ -385,6 +385,57 @@ def process(self, output):
385385
return sysctls
386386

387387

388+
class GroupInfo(TypedDict):
389+
name: str
390+
password: str
391+
gid: int
392+
user_list: list[str]
393+
394+
395+
def _group_info_from_group_str(info: str) -> GroupInfo:
396+
"""
397+
Parses an entry from /etc/group or a similar source, e.g.
398+
'plugdev:x:46:sysadmin,user2' into a GroupInfo dict object
399+
"""
400+
401+
fields = info.split(":")
402+
403+
if len(fields) != 4:
404+
raise ValueError(f"Error parsing group '{info}', expected exactly 4 fields separated by :")
405+
406+
return {
407+
"name": fields[0],
408+
"password": fields[1],
409+
"gid": int(fields[2]),
410+
"user_list": fields[3].split(","),
411+
}
412+
413+
414+
class Group(FactBase[GroupInfo]):
415+
"""
416+
Returns information on a specific group on the system.
417+
"""
418+
419+
def command(self, group):
420+
# FIXME: the '|| true' ensures 'process' is called, even if
421+
# getent was unable to find information on the group
422+
# There must be a better way to do this !
423+
# e.g. allow facts 'process' method access to the process
424+
# return code ?
425+
return f"getent group {group} || true"
426+
427+
default = None
428+
429+
def process(self, output: Iterable[str]) -> str:
430+
group_string = next(iter(output), None)
431+
432+
if group_string is None:
433+
# This will happen if the group was simply not found
434+
return None
435+
436+
return _group_info_from_group_str(group_string)
437+
438+
388439
class Groups(FactBase[List[str]]):
389440
"""
390441
Returns a list of groups on the system.
@@ -417,7 +468,20 @@ def process(self, output) -> list[str]:
417468
Crontab = crontab.Crontab
418469

419470

420-
class Users(FactBase):
471+
class UserInfo(TypedDict):
472+
name: str
473+
comment: str
474+
home: str
475+
shell: str
476+
group: str
477+
groups: list[str]
478+
uid: int
479+
gid: int
480+
lastlog: str
481+
password: str
482+
483+
484+
class Users(FactBase[dict[str, UserInfo]]):
421485
"""
422486
Returns a dictionary of users -> details.
423487
@@ -457,7 +521,7 @@ def command(self) -> str:
457521

458522
default = dict
459523

460-
def process(self, output):
524+
def process(self, output: Iterable[str]) -> dict[str, UserInfo]:
461525
users = {}
462526
rex = r"[A-Z][a-z]{2} [A-Z][a-z]{2} {1,2}\d+ .+$"
463527

tests/facts/server.Group/group.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"arg": "plugdev",
3+
"command": "getent group plugdev || true",
4+
"requires_command": "getent",
5+
"output": [
6+
"plugdev:x:46:sysadmin,myuser,abc"
7+
],
8+
"fact": {
9+
"name": "plugdev",
10+
"password": "x",
11+
"gid": 46,
12+
"user_list": ["sysadmin", "myuser", "abc"]
13+
}
14+
}

tests/facts/server.Group/nogroup.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"arg": "doesnotexist",
3+
"command": "getent group doesnotexist || true",
4+
"requires_command": "getent",
5+
"output": [],
6+
"fact": null
7+
}

tests/test_facts_utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import pytest
2+
3+
from pyinfra.facts.server import _group_info_from_group_str
4+
5+
6+
def test__group_info_from_group_str() -> None:
7+
test_str = "plugdev:x:46:sysadmin,user2"
8+
group_info = _group_info_from_group_str(test_str)
9+
10+
assert group_info["name"] == "plugdev"
11+
assert group_info["password"] == "x"
12+
assert group_info["gid"] == 46
13+
assert group_info["user_list"] == ["sysadmin", "user2"]
14+
15+
16+
def test__group_info_from_group_str_empty() -> None:
17+
with pytest.raises(ValueError):
18+
_group_info_from_group_str("")
19+
20+
with pytest.raises(ValueError):
21+
_group_info_from_group_str("a:b:")

0 commit comments

Comments
 (0)