Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/50351.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
salt-ssh: fix minionfs raising when minions cache dir is missing
14 changes: 14 additions & 0 deletions salt/fileserver/minionfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,13 @@ def file_list(load):
prefix = prefix[len(mountpoint + os.path.sep) :]

minions_cache_dir = os.path.join(__opts__["cachedir"], "minions")
if not os.path.isdir(minions_cache_dir):
# The minions cache dir may not exist yet (e.g. under the salt-ssh
# shim, where the cachedir is a fresh temp dir with no pushed files).
log.debug(
"minionfs: minions cache directory %s does not exist", minions_cache_dir
)
return []
minion_dirs = os.listdir(minions_cache_dir)

# If the prefix is not an empty string, then get the minion id from it. The
Expand Down Expand Up @@ -314,6 +321,13 @@ def dir_list(load):
prefix = prefix[len(mountpoint + os.path.sep) :]

minions_cache_dir = os.path.join(__opts__["cachedir"], "minions")
if not os.path.isdir(minions_cache_dir):
# The minions cache dir may not exist yet (e.g. under the salt-ssh
# shim, where the cachedir is a fresh temp dir with no pushed files).
log.debug(
"minionfs: minions cache directory %s does not exist", minions_cache_dir
)
return []
minion_dirs = os.listdir(minions_cache_dir)

# If the prefix is not an empty string, then get the minion id from it. The
Expand Down
96 changes: 96 additions & 0 deletions tests/pytests/unit/fileserver/test_minionfs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import os

import pytest

import salt.fileserver.minionfs as minionfs


@pytest.fixture
def configure_loader_modules(tmp_path):
opts = {
"cachedir": str(tmp_path),
"minionfs_env": "base",
"minionfs_mountpoint": "",
"minionfs_whitelist": [],
"minionfs_blacklist": [],
"file_ignore_regex": [],
"file_ignore_glob": [],
}
return {minionfs: {"__opts__": opts}}


def test_file_list_missing_minions_cache_dir():
"""
file_list should return an empty list rather than raising when the
minions cache directory does not exist (e.g. under the salt-ssh shim).
"""
minions_cache_dir = os.path.join(minionfs.__opts__["cachedir"], "minions")
assert not os.path.isdir(minions_cache_dir)
assert minionfs.file_list({"saltenv": "base"}) == []


def test_dir_list_missing_minions_cache_dir():
"""
dir_list should return an empty list rather than raising when the
minions cache directory does not exist (e.g. under the salt-ssh shim).
"""
minions_cache_dir = os.path.join(minionfs.__opts__["cachedir"], "minions")
assert not os.path.isdir(minions_cache_dir)
assert minionfs.dir_list({"saltenv": "base"}) == []


def test_file_list_missing_minions_cache_dir_production_load_50351():
"""
file_list must return an empty list rather than raising when the minions
cache directory is absent and the load carries the exact shape production
sends.
"""
# Production callers (fileclient.RemoteClient.file_list ->
# Fileserver.file_list -> backend) always include a "prefix" key (and
# "cmd") in the load. A non-empty prefix is the decisive case: the
# prefix-to-minion-ID handling sits below the os.listdir() call that
# used to raise FileNotFoundError, so it was never reached.
load = {"saltenv": "base", "prefix": "webserver/etc", "cmd": "_file_list"}
minions_cache_dir = os.path.join(minionfs.__opts__["cachedir"], "minions")
assert not os.path.isdir(minions_cache_dir)
assert minionfs.file_list(load) == []


def test_dir_list_missing_minions_cache_dir_production_load_50351():
"""
dir_list must return an empty list rather than raising when the minions
cache directory is absent and the load carries the exact shape production
sends.
"""
# Same production load shape as file_list: fileclient always sends
# "prefix" (and "cmd") alongside "saltenv".
load = {"saltenv": "base", "prefix": "webserver/etc", "cmd": "_dir_list"}
minions_cache_dir = os.path.join(minionfs.__opts__["cachedir"], "minions")
assert not os.path.isdir(minions_cache_dir)
assert minionfs.dir_list(load) == []


def test_file_list_existing_minions_cache_dir_50351(tmp_path):
"""
Guard against overcorrection: when the minions cache directory exists
and holds pushed files, the missing-directory guard must not kick in.
file_list must still return the pushed files.
"""
files_dir = tmp_path / "minions" / "webserver" / "files" / "etc"
files_dir.mkdir(parents=True)
(files_dir / "some.conf").write_text("pushed")
load = {"saltenv": "base", "prefix": "", "cmd": "_file_list"}
assert minionfs.file_list(load) == [os.path.join("webserver", "etc", "some.conf")]


def test_dir_list_existing_minions_cache_dir_50351(tmp_path):
"""
Guard against overcorrection: when the minions cache directory exists
and holds pushed files, the missing-directory guard must not kick in.
dir_list must still return the pushed directories.
"""
files_dir = tmp_path / "minions" / "webserver" / "files" / "etc"
files_dir.mkdir(parents=True)
(files_dir / "some.conf").write_text("pushed")
load = {"saltenv": "base", "prefix": "", "cmd": "_dir_list"}
assert minionfs.dir_list(load) == [os.path.join("webserver", "etc")]
Loading