Skip to content

Commit f9d4dcd

Browse files
committed
feat(core): resolve run_command executables by name (#12)
Executables were run through hardcoded absolute paths, it only works on some distributions. `ss` is in `/usr/bin` on the GitHub Ubuntu runners, instead of in `/usr/sbin` on XCP-ng so the LINSTOR controller tests are marked as failed. So a `find_executable` helper is used now to search for a program in: `/usr/sbin`, `/sbin`, `/usr/bin` and `/bin`. All `_EXEC_PATH_*` are renamed to `_EXECUTABLE_*` and they now hold only a name. Signed-off-by: Ronan Abhamon <ronan.abhamon@vates.tech>
1 parent 85dd4e6 commit f9d4dcd

9 files changed

Lines changed: 71 additions & 30 deletions

File tree

‎semgrep.yml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,21 @@ rules:
3131
- pattern-not-inside: |
3232
class $FLAG(..., IntFlag, ...):
3333
...
34+
35+
- id: python-executable-constant-must-be-a-name
36+
languages: [python]
37+
severity: WARNING
38+
message: >-
39+
`$CONST` must contain only an executable name, not a path: `$VAL`.
40+
patterns:
41+
- pattern-either:
42+
- pattern: "$CONST = $VAL"
43+
- pattern: "$CONST: $TYPE = $VAL"
44+
45+
- metavariable-regex:
46+
metavariable: $CONST
47+
regex: "^_?EXECUTABLE_[A-Z0-9_]+$"
48+
49+
- metavariable-regex:
50+
metavariable: $VAL
51+
regex: "^[\"'](?![A-Za-z0-9][A-Za-z0-9._+-]*[\"']$)"

‎src/xcp_storage/backends/drbd/__init__.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444

4545
# ------------------------------------------------------------------------------
4646

47-
_EXEC_PATH_DRBDSETUP: Final = "/usr/sbin/drbdsetup"
47+
_EXECUTABLE_DRBDSETUP: Final = "drbdsetup"
4848

4949
_REGEX_DRBD_OPENER_LINE: Final = re.compile(r"(.*)\s+(\d+)\s+(\d+)")
5050

@@ -90,7 +90,7 @@ def _handle_drbd_json_error() -> Iterator[None]:
9090
def _get_drbd_status(resource_name: str) -> Dict[str, Any]:
9191
try:
9292
stdout, stderr, ret_code = run_command([
93-
_EXEC_PATH_DRBDSETUP, "status", resource_name, "--json"
93+
_EXECUTABLE_DRBDSETUP, "status", resource_name, "--json"
9494
], simple=False)
9595
if ret_code != 0:
9696
logger.warning(
@@ -224,7 +224,7 @@ def demote(resource_name: str) -> bool:
224224
_check_drbd_resource_name(resource_name)
225225
error_message = ""
226226
try:
227-
_stdout, stderr, ret_code = run_command([_EXEC_PATH_DRBDSETUP, "secondary", resource_name], simple=False)
227+
_stdout, stderr, ret_code = run_command([_EXECUTABLE_DRBDSETUP, "secondary", resource_name], simple=False)
228228
if not ret_code:
229229
return True
230230
error_message = stderr

‎src/xcp_storage/backends/linstor/controller.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
# ------------------------------------------------------------------------------
3232

33-
_EXEC_PATH_SS: Final = "/usr/sbin/ss"
33+
_EXECUTABLE_SS: Final = "ss"
3434

3535
_SERVICE_LINSTOR_CONTROLLER: Final = "linstor-controller"
3636

@@ -40,7 +40,7 @@ class LinstorController:
4040
@staticmethod
4141
def get_addresses() -> List[str]:
4242
stdout = run_command([
43-
_EXEC_PATH_SS, "-tnpH", "state", "established",
43+
_EXECUTABLE_SS, "-tnpH", "state", "established",
4444
f"( sport = :{LINSTOR_SATELLITE_PORT_PLAIN} or sport = :{LINSTOR_SATELLITE_PORT_SSL} )"
4545
], expected_ret_code=0)
4646
return [

‎src/xcp_storage/network/iptables.py‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626

2727
_MAIN_FIREWALL_INPUT_CHAIN: Final = "INPUT"
2828

29-
_EXEC_PATH_IPTABLES: Final = "/usr/sbin/iptables"
30-
_EXEC_PATH_SERVICE: Final = "/usr/sbin/service"
29+
_EXECUTABLE_IPTABLES: Final = "iptables"
30+
_EXECUTABLE_SERVICE: Final = "service"
3131

3232
_PROTOCOL_TCP: Final = "tcp"
3333

@@ -42,7 +42,7 @@ def __init__(self, message: str, code: Optional[int] = None) -> None:
4242

4343
def _save_iptables_changes() -> None:
4444
try:
45-
run_command([_EXEC_PATH_SERVICE, "iptables", "save"], expected_ret_code=0)
45+
run_command([_EXECUTABLE_SERVICE, "iptables", "save"], expected_ret_code=0)
4646
except CommandError as e:
4747
raise IptablesError(f"Failed to save iptables changes: `{e.reason}`.", e.code) from None
4848

@@ -62,7 +62,7 @@ def _is_fatal_iptables_code(code: int) -> bool:
6262
def has_iptables_rule(rule: List[str]) -> bool:
6363
code: Optional[int]
6464
try:
65-
_stdout, reason, code = run_command([_EXEC_PATH_IPTABLES, "-C"] + rule, simple=False)
65+
_stdout, reason, code = run_command([_EXECUTABLE_IPTABLES, "-C"] + rule, simple=False)
6666
if not _is_fatal_iptables_code(code):
6767
return not code
6868
except CommandError as e:
@@ -73,19 +73,19 @@ def has_iptables_rule(rule: List[str]) -> bool:
7373

7474
def _ensure_iptables_chain_exists(chain: str) -> bool:
7575
try:
76-
_stdout, _stderr, ret_code = run_command([_EXEC_PATH_IPTABLES, "-N", chain], simple=False)
76+
_stdout, _stderr, ret_code = run_command([_EXECUTABLE_IPTABLES, "-N", chain], simple=False)
7777
if _is_fatal_iptables_code(ret_code):
7878
raise IptablesError(f"Failed to test existence of iptables chain `{chain}`: `{_stderr}`.", ret_code)
7979
updated = not ret_code
8080

8181
return_rule = [chain, "-j", "RETURN"]
8282
if not has_iptables_rule(return_rule):
83-
run_command([_EXEC_PATH_IPTABLES, "-A"] + return_rule, expected_ret_code=0)
83+
run_command([_EXECUTABLE_IPTABLES, "-A"] + return_rule, expected_ret_code=0)
8484
updated = True
8585

8686
jump_rule = [_MAIN_FIREWALL_INPUT_CHAIN, "-j", chain]
8787
if not has_iptables_rule(jump_rule):
88-
run_command([_EXEC_PATH_IPTABLES, "-I"] + jump_rule, expected_ret_code=0)
88+
run_command([_EXECUTABLE_IPTABLES, "-I"] + jump_rule, expected_ret_code=0)
8989
updated = True
9090
except CommandError as e:
9191
raise IptablesError(f"Failed to set up iptables chain `{chain}`: `{e.reason}`.", e.code) from None
@@ -107,14 +107,14 @@ def _update_iptables_ports(protocol: str, str_ports: str, *, open_ports: bool, s
107107
if open_ports:
108108
_ensure_iptables_chain_exists(chain)
109109
try:
110-
run_command([_EXEC_PATH_IPTABLES, "-I"] + rule, expected_ret_code=0)
110+
run_command([_EXECUTABLE_IPTABLES, "-I"] + rule, expected_ret_code=0)
111111
except CommandError as e:
112112
raise IptablesError(
113113
f"Failed to open {protocol.upper()} port(s): `{e.reason}`.", e.code
114114
) from None
115115
else:
116116
try:
117-
run_command([_EXEC_PATH_IPTABLES, "-D"] + rule, expected_ret_code=0)
117+
run_command([_EXECUTABLE_IPTABLES, "-D"] + rule, expected_ret_code=0)
118118
except CommandError as e:
119119
raise IptablesError(
120120
f"Failed to close {protocol.upper()} port(s): `{e.reason}`.", e.code

‎src/xcp_storage/utils/process.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
# You should have received a copy of the GNU General Public License
1313
# along with this program. If not, see <https://www.gnu.org/licenses/>.
1414

15+
import os
1516
from pathlib import Path
17+
import shutil
1618
import subprocess
1719

1820
import xcp_storage.log as log
1921

2022
from xcp_storage.typing import (
2123
Callable,
24+
Final,
2225
List,
2326
Literal,
2427
Optional,
@@ -33,6 +36,14 @@
3336

3437
# ------------------------------------------------------------------------------
3538

39+
# Trusted directories, searched in this order, to find the executables run by `run_command`.
40+
_EXECUTABLE_DIRS: Final = ("/usr/sbin", "/usr/bin", "/sbin", "/bin")
41+
42+
def find_executable(name: str) -> Optional[str]:
43+
return shutil.which(name, path=os.pathsep.join(_EXECUTABLE_DIRS))
44+
45+
# ------------------------------------------------------------------------------
46+
3647
class CommandError(Exception):
3748
def __init__(self, code: Optional[int], cmd: str, reason: str) -> None:
3849
super().__init__("Command execution error.")
@@ -90,6 +101,17 @@ def run_internal_command(
90101
ret_code_callback: Callable[[str, str, int], int] = default_ret_code_callback,
91102
quiet: bool = False
92103
) -> CommandResultType:
104+
executable_name = args[0]
105+
106+
# A bare program name is resolved to an absolute path; an explicit path is kept as is.
107+
if os.sep not in executable_name:
108+
exec_path = find_executable(executable_name)
109+
if exec_path is None:
110+
raise CommandError(
111+
None, str(args), reason=f"Executable `{executable_name}` not found in `{', '.join(_EXECUTABLE_DIRS)}`."
112+
)
113+
args = [exec_path, *args[1:]]
114+
93115
try:
94116
# TODO(XCPNG-3032): Remove noqa warn and use `capture_output` after we discontinue support for Python 3.6.
95117
result = subprocess.run( # noqa: UP022

‎src/xcp_storage/utils/service.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
# ==============================================================================
2424

25-
_EXEC_PATH_SYSTEMCTL: Final = "/usr/bin/systemctl"
25+
_EXECUTABLE_SYSTEMCTL: Final = "systemctl"
2626

2727
# ------------------------------------------------------------------------------
2828

@@ -34,7 +34,7 @@ def __init__(self, message: str, code: Optional[int] = None) -> None:
3434
# ------------------------------------------------------------------------------
3535

3636
def _run_service_command(service_name: str, action: List[str], *, quiet: bool = False) -> None:
37-
args = [_EXEC_PATH_SYSTEMCTL, *action, service_name]
37+
args = [_EXECUTABLE_SYSTEMCTL, *action, service_name]
3838
try:
3939
run_command(args, expected_ret_code=0, quiet=quiet)
4040
except CommandError as e:
@@ -77,6 +77,6 @@ def disable_and_stop_service(service_name: str) -> None:
7777

7878
def reload_service_conf() -> None:
7979
try:
80-
run_command([_EXEC_PATH_SYSTEMCTL, "daemon-reload"], expected_ret_code=0)
80+
run_command([_EXECUTABLE_SYSTEMCTL, "daemon-reload"], expected_ret_code=0)
8181
except CommandError as e:
8282
raise ServiceError(e.reason, e.code) from None

‎tests/backends/test_drbd.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
from xcp_storage.backends.drbd import (
2424
_get_drbd_status,
25+
_EXECUTABLE_DRBDSETUP,
2526
Drbd,
2627
DrbdOpener,
2728
)
@@ -87,7 +88,7 @@ def test_success(self, mock_run_command: MagicMock, drbd_json_status_primary: st
8788
assert status["name"] == "xcp-volume-patate"
8889
assert status["role"] == "Primary"
8990
mock_run_command.assert_called_once_with(
90-
["/usr/sbin/drbdsetup", "status", "xcp-volume-patate", "--json"], simple=False
91+
[_EXECUTABLE_DRBDSETUP, "status", "xcp-volume-patate", "--json"], simple=False
9192
)
9293

9394
def test_command_failure(self, mock_run_command: MagicMock, caplog: pytest.LogCaptureFixture) -> None:

‎tests/network/test_iptables.py‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
import pytest
2323

2424
from xcp_storage.network.iptables import (
25-
_EXEC_PATH_IPTABLES,
26-
_EXEC_PATH_SERVICE,
25+
_EXECUTABLE_IPTABLES,
26+
_EXECUTABLE_SERVICE,
2727
_MAIN_FIREWALL_INPUT_CHAIN,
2828
_PROTOCOL_TCP,
2929
DEFAULT_FIREWALL_INPUT_CHAIN,
@@ -74,22 +74,22 @@ def get_tcp_rule(ports_str: str, *, stateful: bool = True, chain: str = DEFAULT_
7474
# ------------------------------------------------------------------------------
7575

7676
def get_has_chain_cmd(chain: str = DEFAULT_FIREWALL_INPUT_CHAIN) -> List[str]:
77-
return [_EXEC_PATH_IPTABLES, "-N", chain]
77+
return [_EXECUTABLE_IPTABLES, "-N", chain]
7878

7979
def get_has_rule_cmd(rule: List[str]) -> List[str]:
80-
return [_EXEC_PATH_IPTABLES, "-C"] + rule
80+
return [_EXECUTABLE_IPTABLES, "-C"] + rule
8181

8282
def get_add_rule_cmd(rule: List[str]) -> List[str]:
83-
return [_EXEC_PATH_IPTABLES, "-A"] + rule
83+
return [_EXECUTABLE_IPTABLES, "-A"] + rule
8484

8585
def get_insert_rule_cmd(rule: List[str]) -> List[str]:
86-
return [_EXEC_PATH_IPTABLES, "-I"] + rule
86+
return [_EXECUTABLE_IPTABLES, "-I"] + rule
8787

8888
def get_destroy_rule_cmd(rule: List[str]) -> List[str]:
89-
return [_EXEC_PATH_IPTABLES, "-D"] + rule
89+
return [_EXECUTABLE_IPTABLES, "-D"] + rule
9090

9191
def get_save_iptables_cmd() -> List[str]:
92-
return [_EXEC_PATH_SERVICE, "iptables", "save"]
92+
return [_EXECUTABLE_SERVICE, "iptables", "save"]
9393

9494
# ------------------------------------------------------------------------------
9595

@@ -159,7 +159,7 @@ def impl(*args: P.args, **kwargs: P.kwargs) -> CommandResultType:
159159
cmd_args = cast(List[str], args[0])
160160

161161
program_name = cmd_args[0]
162-
assert program_name in (_EXEC_PATH_IPTABLES, _EXEC_PATH_SERVICE)
162+
assert program_name in (_EXECUTABLE_IPTABLES, _EXECUTABLE_SERVICE)
163163

164164
simple = kwargs.get("simple", True)
165165

‎tests/utils/test_service.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
)
1919

2020
from xcp_storage.utils.service import (
21-
_EXEC_PATH_SYSTEMCTL,
21+
_EXECUTABLE_SYSTEMCTL,
2222
disable_and_stop_service,
2323
enable_and_start_service,
2424
escape_service_instance,
@@ -54,7 +54,7 @@ def _assert_service_command(
5454
quiet: bool
5555
) -> None:
5656
args.append(self.SERVICE_NAME)
57-
args.insert(0, _EXEC_PATH_SYSTEMCTL)
57+
args.insert(0, _EXECUTABLE_SYSTEMCTL)
5858
mock_run_command.assert_called_once_with(args, expected_ret_code=expected_ret_code, quiet=quiet)
5959

6060
def _assert_query_command(self, mock_run_command: MagicMock, args: List[str]) -> None:
@@ -97,4 +97,4 @@ def test_disable_and_stop_service(self, mock_run_command: MagicMock) -> None:
9797

9898
def test_reload_service_conf(self, mock_run_command: MagicMock) -> None:
9999
reload_service_conf()
100-
mock_run_command.assert_called_once_with([_EXEC_PATH_SYSTEMCTL, "daemon-reload"], expected_ret_code=0)
100+
mock_run_command.assert_called_once_with([_EXECUTABLE_SYSTEMCTL, "daemon-reload"], expected_ret_code=0)

0 commit comments

Comments
 (0)