Skip to content

Commit c8c05b3

Browse files
feat: Read Minion ID from Client's salt conf (#179)
1 parent 3bbd6b4 commit c8c05b3

File tree

4 files changed

+204
-0
lines changed

4 files changed

+204
-0
lines changed

examples/general/read_minion_id.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Example to demonstrate reading the minion ID from the Salt configuration."""
2+
3+
from nisystemlink.clients.core.helpers import read_minion_id
4+
5+
# Read the minion ID from the Salt configuration file
6+
minion_id = read_minion_id()
7+
8+
if minion_id:
9+
print(f"Minion ID: {minion_id}")
10+
else:
11+
print(
12+
"Minion ID not found. Please ensure the SystemLink client is connected to the Server."
13+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from ._iterator_file_like import IteratorFileLike
2+
from ._minion_id import read_minion_id
23

34
# flake8: noqa
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""Helper function to get minion ID from Salt configuration."""
4+
5+
from nisystemlink.clients.core._internal._path_constants import PathConstants
6+
7+
8+
def read_minion_id() -> str | None:
9+
"""Read the minion ID from the Salt configuration.
10+
11+
Returns:
12+
str | None: The minion ID content if the file exists, None otherwise.
13+
"""
14+
minion_id_path = PathConstants.salt_data_directory / "conf" / "minion_id"
15+
16+
if not minion_id_path.exists():
17+
return None
18+
19+
try:
20+
with open(minion_id_path, "r", encoding="utf-8") as fp:
21+
return fp.read().strip()
22+
except (OSError, PermissionError):
23+
return None

tests/core/test_minion_id.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""Tests for read_minion_id helper function."""
4+
5+
from pathlib import Path
6+
from unittest import mock
7+
8+
import pytest
9+
from nisystemlink.clients.core.helpers._minion_id import read_minion_id
10+
11+
12+
@pytest.fixture
13+
def mock_path_constants():
14+
"""Fixture to mock PathConstants."""
15+
with mock.patch(
16+
"nisystemlink.clients.core.helpers._minion_id.PathConstants"
17+
) as mock_constants:
18+
yield mock_constants
19+
20+
21+
@pytest.fixture
22+
def mock_minion_id_path(mock_path_constants):
23+
"""Fixture to set up the mock path structure for minion_id file.
24+
25+
Returns a callable that accepts an exists parameter to configure
26+
whether the path exists.
27+
"""
28+
29+
def _create_mock_path(exists=True):
30+
mock_minion_id_path = mock.Mock(spec=Path)
31+
mock_minion_id_path.exists.return_value = exists
32+
33+
# Set up the path chain: salt_data_directory / "conf" / "minion_id"
34+
mock_conf_path = mock.Mock(spec=Path)
35+
mock_conf_path.__truediv__ = mock.Mock(return_value=mock_minion_id_path)
36+
37+
mock_salt_dir = mock.Mock(spec=Path)
38+
mock_salt_dir.__truediv__ = mock.Mock(return_value=mock_conf_path)
39+
40+
mock_path_constants.salt_data_directory = mock_salt_dir
41+
42+
return mock_minion_id_path
43+
44+
return _create_mock_path
45+
46+
47+
class TestReadMinionId:
48+
"""Test cases for read_minion_id function."""
49+
50+
def test__minion_id_file_exists__returns_content(self, mock_minion_id_path):
51+
"""Test that the minion ID is read correctly when the file exists."""
52+
# Arrange
53+
expected_minion_id = "test-minion-123"
54+
minion_path = mock_minion_id_path(exists=True)
55+
56+
# Mock the file open and read
57+
mock_open = mock.mock_open(read_data=f"{expected_minion_id}\n")
58+
59+
# Act
60+
with mock.patch("builtins.open", mock_open):
61+
result = read_minion_id()
62+
63+
# Assert
64+
assert result == expected_minion_id
65+
minion_path.exists.assert_called_once()
66+
mock_open.assert_called_once_with(minion_path, "r", encoding="utf-8")
67+
68+
def test__minion_id_file_exists_with_whitespace__returns_stripped_content(
69+
self, mock_minion_id_path
70+
):
71+
"""Test that the minion ID is stripped of leading/trailing whitespace."""
72+
# Arrange
73+
expected_minion_id = "test-minion-456"
74+
minion_path = mock_minion_id_path(exists=True)
75+
76+
# Mock the file open with extra whitespace
77+
mock_open = mock.mock_open(read_data=f" {expected_minion_id} \n\t")
78+
79+
# Act
80+
with mock.patch("builtins.open", mock_open):
81+
result = read_minion_id()
82+
83+
# Assert
84+
assert result == expected_minion_id
85+
minion_path.exists.assert_called_once()
86+
87+
def test__minion_id_file_does_not_exist__returns_none(self, mock_minion_id_path):
88+
"""Test that None is returned when the minion_id file does not exist."""
89+
# Arrange
90+
minion_path = mock_minion_id_path(exists=False)
91+
92+
# Act
93+
result = read_minion_id()
94+
95+
# Assert
96+
assert result is None
97+
minion_path.exists.assert_called_once()
98+
99+
def test__minion_id_file_has_oserror__returns_none(self, mock_minion_id_path):
100+
"""Test that None is returned when an OSError occurs reading the file."""
101+
# Arrange
102+
minion_path = mock_minion_id_path(exists=True)
103+
104+
# Mock the file open to raise OSError
105+
mock_open = mock.mock_open()
106+
mock_open.side_effect = OSError("File access error")
107+
108+
# Act
109+
with mock.patch("builtins.open", mock_open):
110+
result = read_minion_id()
111+
112+
# Assert
113+
assert result is None
114+
minion_path.exists.assert_called_once()
115+
116+
def test__minion_id_file_has_permission_error__returns_none(
117+
self, mock_minion_id_path
118+
):
119+
"""Test that None is returned when a PermissionError occurs reading the file."""
120+
# Arrange
121+
minion_path = mock_minion_id_path(exists=True)
122+
123+
# Mock the file open to raise PermissionError
124+
mock_open = mock.mock_open()
125+
mock_open.side_effect = PermissionError("Permission denied")
126+
127+
# Act
128+
with mock.patch("builtins.open", mock_open):
129+
result = read_minion_id()
130+
131+
# Assert
132+
assert result is None
133+
minion_path.exists.assert_called_once()
134+
135+
def test__minion_id_file_is_empty__returns_empty_string(self, mock_minion_id_path):
136+
"""Test that an empty string is returned when the file is empty."""
137+
# Arrange
138+
minion_path = mock_minion_id_path(exists=True)
139+
140+
# Mock the file open with empty content
141+
mock_open = mock.mock_open(read_data="")
142+
143+
# Act
144+
with mock.patch("builtins.open", mock_open):
145+
result = read_minion_id()
146+
147+
# Assert
148+
assert result == ""
149+
minion_path.exists.assert_called_once()
150+
151+
def test__minion_id_file_only_whitespace__returns_empty_string(
152+
self, mock_minion_id_path
153+
):
154+
"""Test that an empty string is returned when the file contains only whitespace."""
155+
# Arrange
156+
minion_path = mock_minion_id_path(exists=True)
157+
158+
# Mock the file open with only whitespace
159+
mock_open = mock.mock_open(read_data=" \n\t \n")
160+
161+
# Act
162+
with mock.patch("builtins.open", mock_open):
163+
result = read_minion_id()
164+
165+
# Assert
166+
assert result == ""
167+
minion_path.exists.assert_called_once()

0 commit comments

Comments
 (0)