/
githubmirror
/
salt
Обзор
Документация
Войти
/
githubmirror
/
salt
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
tests/pytests/unit/client/ssh/wrapper/test_cp.py
1 145 строк
40 KB
Daniel A. Wozniak
Fix tests referencing repo.saltproject.io
07 фев 2025, 06:48
07 фев 2025, 06:48
fe28c55
Код
Авторство
О чём код?
""" Test the SSHCpClient. The following tests are adapted from tests.pytests.unit.fileclient.test_fileclient_cache. + additional ones below """ import errno import logging import os import shlex import shutil from pathlib import Path import pytest import salt.client.ssh.shell import salt.exceptions import salt.utils.files from salt.client.ssh.wrapper import cp from tests.support.mock import Mock, call, patch log = logging.getLogger(__name__) pytestmark = [pytest.mark.skip_on_windows] SUBDIR = "subdir" TGT = "testtarget" def _saltenvs(): return ("base", "dev") def _subdir_files(): return ("foo.txt", "bar.txt", "baz.txt") def _get_file_roots(fs_root): return {x: [os.path.join(fs_root, x)] for x in _saltenvs()} @pytest.fixture def fs_root(tmp_path): return os.path.join(tmp_path, "fileclient_fs_root") @pytest.fixture def cache_root(tmp_path): return os.path.join(tmp_path, "fileclient_cache_root") @pytest.fixture def remote_list(cache_root): cache_root = Path(cache_root) return { "files": { x for env in _saltenvs() for x in ( Path("/tmp/targetdir/existingfile"), cache_root / f"files/{env}/filetodir", cache_root / f"extrn_files/{env}/filetodir", cache_root / f"files/{env}/foo.sls", cache_root / f"files/{env}/nested/path/foo.sls", cache_root / f"extrn_files/{env}/bar.sls", cache_root / f"files/{env}/rmfail/rmfailfile", cache_root / "localfiles/this/file/was/cached/locally", ) }, "dirs": { x for env in _saltenvs() for x in ( (Path("/tmp/targetdir"),) + tuple((cache_root / f"extrn_files/{env}/dummy").parents) + tuple((cache_root / f"files/{env}/nested/path/dummy").parents) + tuple((cache_root / f"files/{env}/dirtofile/dummy").parents) + tuple((cache_root / f"extrn_files/{env}/dirtofile/dummy").parents) + tuple((cache_root / f"files/{env}/rmfail/dummy").parents) + tuple( ( cache_root / f"localfiles/{env}/this/file/was/cached/locally" ).parents ) ) }, "send_fail": { x for env in _saltenvs() for x in ( Path("/tmp/targetdir/failfile"), cache_root / f"files/{env}/{SUBDIR}/fail.sls", ) }, "rm_fail": { x for env in _saltenvs() for x in ( cache_root / f"files/{env}/rmfail", cache_root / f"files/{env}/rmfail/rmfailfile", ) }, } @pytest.fixture def shell(request): remote_list = getattr(request, "param", None) removed = set() if remote_list is None: remote_list = request.getfixturevalue("remote_list") def exec_cmd(cmd): cmd = shlex.split(cmd) if cmd[0] == "rm": path = Path(cmd[2]) if path in remote_list["rm_fail"]: return "", "you shall not pass", 1 if cmd[1] == "-rf": path = Path(cmd[2]) if path in removed: return "", "deleted a path twice", 1 removed.add(path) return "", "", 0 if cmd[0] != "test": return "", "", 0 if cmd[1] == "-d": return "", "", int(Path(cmd[2]) not in remote_list["dirs"]) if cmd[1] == "-f": return "", "", int(Path(cmd[2]) not in remote_list["files"]) if cmd[1] == "-e": return ( "", "", int( Path(cmd[2]) not in remote_list["files"] and Path(cmd[2]) not in remote_list["dirs"] ), ) return "", "", 0 def send(src, dest, makedirs=False): dest = Path(dest) if dest in remote_list["send_fail"] or str(dest).endswith("fail"): return "", "sth went wrong", 1 if any(x in remote_list["files"] and x not in removed for x in dest.parents): return "", "path contains files which were not removed: Not a directory", 1 if dest.parent not in remote_list["dirs"] and not makedirs: return "", "tgt dir does not exist, no makedirs", 1 if dest in remote_list["dirs"] and dest not in removed: # send should always receive the full path of the file return "", "just copied a file into a dir", 1 return "", "", 0 shl = Mock(spec=salt.client.ssh.shell.Shell) shl.exec_cmd.side_effect = exec_cmd shl.send.side_effect = send return shl @pytest.fixture def mocked_opts(tmp_path, fs_root, cache_root): return { "file_roots": _get_file_roots(fs_root), "fileserver_backend": ["roots"], "cachedir": cache_root, "file_client": "local", } @pytest.fixture def _setup(fs_root, cache_root): """ No need to add a dummy foo.txt to muddy up the github repo, just make our own fileserver root on-the-fly. """ def _new_dir(path): """ Add a new dir at ``path`` using os.makedirs. If the directory already exists, remove it recursively and then try to create it again. """ try: os.makedirs(path) except OSError as exc: if exc.errno == errno.EEXIST: # Just in case a previous test was interrupted, remove the # directory and try adding it again. shutil.rmtree(path) os.makedirs(path) else: raise # Crete the FS_ROOT for saltenv in _saltenvs(): saltenv_root = os.path.join(fs_root, saltenv) # Make sure we have a fresh root dir for this saltenv _new_dir(saltenv_root) path = os.path.join(saltenv_root, "foo.txt") with salt.utils.files.fopen(path, "w") as fp_: fp_.write(f"This is a test file in the '{saltenv}' saltenv.\n") (Path(saltenv_root) / "dirtofile").touch() (Path(saltenv_root) / "filetodir").mkdir() (Path(saltenv_root) / "filetodir" / "foo.sh").touch() subdir_abspath = os.path.join(saltenv_root, SUBDIR) os.makedirs(subdir_abspath) for subdir_file in _subdir_files(): path = os.path.join(subdir_abspath, subdir_file) with salt.utils.files.fopen(path, "w") as fp_: fp_.write( "This is file '{}' in subdir '{} from saltenv '{}'".format( subdir_file, SUBDIR, saltenv ) ) (Path(subdir_abspath) / "fail.sls").touch() # Create the CACHE_ROOT _new_dir(cache_root) @pytest.fixture def client(minion_opts, mocked_opts, shell): patched_opts = minion_opts.copy() patched_opts.update(mocked_opts) return cp.SSHCpClient(patched_opts, shell, TGT) @pytest.mark.usefixtures("_setup") def test_cache_dir(client, cache_root): """ Ensure entire directory is cached to correct location """ for saltenv in _saltenvs(): assert client.cache_dir(f"salt://{SUBDIR}", saltenv, cachedir=None) for subdir_file in _subdir_files(): cache_loc = os.path.join( cache_root, "salt-ssh", TGT, "files", saltenv, SUBDIR, subdir_file, ) # Double check that the content of the cached file # identifies it as being from the correct saltenv. The # setUp function creates the file with the name of the # saltenv mentioned in the file, so a simple 'in' check is # sufficient here. If opening the file raises an exception, # this is a problem, so we are not catching the exception # and letting it be raised so that the test fails. with salt.utils.files.fopen(cache_loc) as fp_: content = fp_.read() log.debug("cache_loc = %s", cache_loc) log.debug("content = %s", content) assert subdir_file in content assert SUBDIR in content assert saltenv in content minion_cache_loc = os.path.join( cache_root, "files", saltenv, SUBDIR, subdir_file, ) client.shell.send.assert_any_call(cache_loc, minion_cache_loc, True) @pytest.mark.usefixtures("_setup") def test_cache_dir_with_alternate_cachedir_and_absolute_path( client, tmp_path, cache_root ): """ Ensure entire directory is cached to the default location when an alternate cachedir is specified and that cachedir is an absolute path - but then sent to the correct cachedir on the minion. """ alt_cachedir = os.path.join(tmp_path, "abs_cachedir") for saltenv in _saltenvs(): assert client.cache_dir(f"salt://{SUBDIR}", saltenv, cachedir=alt_cachedir) for subdir_file in _subdir_files(): cache_loc = os.path.join( cache_root, "salt-ssh", TGT, "absolute_root", alt_cachedir[1:], "files", saltenv, SUBDIR, subdir_file, ) # Double check that the content of the cached file # identifies it as being from the correct saltenv. The # setUp function creates the file with the name of the # saltenv mentioned in the file, so a simple 'in' check is # sufficient here. If opening the file raises an exception, # this is a problem, so we are not catching the exception # and letting it be raised so that the test fails. with salt.utils.files.fopen(cache_loc) as fp_: content = fp_.read() log.debug("cache_loc = %s", cache_loc) log.debug("content = %s", content) assert subdir_file in content assert SUBDIR in content assert saltenv in content minion_cache_loc = os.path.join( alt_cachedir, "files", saltenv, SUBDIR, subdir_file ) client.shell.send.assert_any_call(cache_loc, minion_cache_loc, True) @pytest.mark.usefixtures("_setup") def test_cache_dir_with_alternate_cachedir_and_relative_path(client, cache_root): """ Ensure entire directory is cached to correct location when an alternate cachedir is specified and that cachedir is a relative path """ alt_cachedir = "foo" for saltenv in _saltenvs(): assert client.cache_dir(f"salt://{SUBDIR}", saltenv, cachedir=alt_cachedir) for subdir_file in _subdir_files(): cache_loc = os.path.join( cache_root, "salt-ssh", TGT, alt_cachedir, "files", saltenv, SUBDIR, subdir_file, ) # Double check that the content of the cached file # identifies it as being from the correct saltenv. The # setUp function creates the file with the name of the # saltenv mentioned in the file, so a simple 'in' check is # sufficient here. If opening the file raises an exception, # this is a problem, so we are not catching the exception # and letting it be raised so that the test fails. with salt.utils.files.fopen(cache_loc) as fp_: content = fp_.read() log.debug("cache_loc = %s", cache_loc) log.debug("content = %s", content) assert subdir_file in content assert SUBDIR in content assert saltenv in content minion_cache_loc = os.path.join( cache_root, alt_cachedir, "files", saltenv, SUBDIR, subdir_file, ) client.shell.send.assert_any_call(cache_loc, minion_cache_loc, True) @pytest.mark.usefixtures("_setup") def test_cache_file(client, cache_root): """ Ensure file is cached to correct location """ for saltenv in _saltenvs(): assert client.cache_file("salt://foo.txt", saltenv, cachedir=None) cache_loc = os.path.join( cache_root, "salt-ssh", TGT, "files", saltenv, "foo.txt" ) # Double check that the content of the cached file identifies # it as being from the correct saltenv. The setUp function # creates the file with the name of the saltenv mentioned in # the file, so a simple 'in' check is sufficient here. If # opening the file raises an exception, this is a problem, so # we are not catching the exception and letting it be raised so # that the test fails. with salt.utils.files.fopen(cache_loc) as fp_: content = fp_.read() log.debug("cache_loc = %s", cache_loc) log.debug("content = %s", content) assert saltenv in content minion_cache_loc = os.path.join(cache_root, "files", saltenv, "foo.txt") client.shell.send.assert_any_call(cache_loc, minion_cache_loc, True) @pytest.mark.usefixtures("_setup") def test_cache_file_with_alternate_cachedir_and_absolute_path( client, tmp_path, cache_root ): """ Ensure file is cached to the default location when an alternate cachedir is specified and that cachedir is an absolute path, but then sent to the correct path on the minion """ alt_cachedir = os.path.join(tmp_path, "abs_cachedir") for saltenv in _saltenvs(): assert client.cache_file("salt://foo.txt", saltenv, cachedir=alt_cachedir) cache_loc = os.path.join( cache_root, "salt-ssh", TGT, "absolute_root", alt_cachedir[1:], "files", saltenv, "foo.txt", ) # Double check that the content of the cached file identifies # it as being from the correct saltenv. The setUp function # creates the file with the name of the saltenv mentioned in # the file, so a simple 'in' check is sufficient here. If # opening the file raises an exception, this is a problem, so # we are not catching the exception and letting it be raised so # that the test fails. with salt.utils.files.fopen(cache_loc) as fp_: content = fp_.read() log.debug("cache_loc = %s", cache_loc) log.debug("content = %s", content) assert saltenv in content minion_cache_loc = os.path.join(alt_cachedir, "files", saltenv, "foo.txt") client.shell.send.assert_any_call(cache_loc, minion_cache_loc, True) @pytest.mark.usefixtures("_setup") def test_cache_file_with_alternate_cachedir_and_relative_path(client, cache_root): """ Ensure file is cached to correct location when an alternate cachedir is specified and that cachedir is a relative path """ alt_cachedir = "foo" for saltenv in _saltenvs(): assert client.cache_file("salt://foo.txt", saltenv, cachedir=alt_cachedir) cache_loc = os.path.join( cache_root, "salt-ssh", TGT, alt_cachedir, "files", saltenv, "foo.txt", ) # Double check that the content of the cached file identifies # it as being from the correct saltenv. The setUp function # creates the file with the name of the saltenv mentioned in # the file, so a simple 'in' check is sufficient here. If # opening the file raises an exception, this is a problem, so # we are not catching the exception and letting it be raised so # that the test fails. with salt.utils.files.fopen(cache_loc) as fp_: content = fp_.read() log.debug("cache_loc = %s", cache_loc) log.debug("content = %s", content) assert saltenv in content minion_cache_loc = os.path.join( cache_root, alt_cachedir, "files", saltenv, "foo.txt", ) client.shell.send.assert_any_call(cache_loc, minion_cache_loc, True) @pytest.mark.usefixtures("_setup") def test_cache_dest(client, cache_root): """ Tests functionality for cache_dest """ relpath = "foo.com/bar.txt" def _external(saltenv="base"): return salt.utils.path.join( cache_root, "salt-ssh", TGT, "extrn_files", saltenv, relpath ) def _salt(saltenv="base"): return salt.utils.path.join( cache_root, "salt-ssh", TGT, "files", saltenv, relpath ) def _check(ret, expected): assert ret == expected, f"{ret} != {expected}" _check(client.cache_dest(f"https://{relpath}"), _external()) _check(client.cache_dest(f"https://{relpath}", "dev"), _external("dev")) _check(client.cache_dest(f"salt://{relpath}"), _salt()) _check(client.cache_dest(f"salt://{relpath}", "dev"), _salt("dev")) _check(client.cache_dest(f"salt://{relpath}?saltenv=dev"), _salt("dev")) _check("/foo/bar", "/foo/bar") # ------ END of adaptation of existing fileclient tests ------ # The following tests are for the client specifically, the wrapper # function tests are implemented as integration ones. def test_cache_local_file(client): """ Caching local files would currently mean extracting files from the master to the minion, something that is likely very unexpected. Semantically, the other way around would be more expected, but receiving files from the minion is currently not implemented and might not be the best idea from a security perspective. """ with pytest.raises( salt.exceptions.CommandExecutionError, match="Cannot cache local files via salt-ssh", ): client.cache_local_file("/foo/bar") @pytest.mark.parametrize("master", (False, True)) @pytest.mark.parametrize("cachedir", (None, "", "relative/path", "/absolute/path")) def test_get_cachedir(client, cachedir, master, cache_root): """ Ensure that the cachedirs are correctly returned and that the master root cannot be overridden with an absolute path. """ subdir = cachedir if cachedir is None: base = cache_root elif cachedir.startswith("/"): if master: base = cache_root subdir = "absolute_root" + cachedir else: base = cachedir else: base = cache_root if master: expected = str(Path(base) / "salt-ssh" / TGT / (subdir or "")).rstrip("/") else: expected = str(Path(base) / (subdir or "")).rstrip("/") if cachedir == "": # The usual fileclient does it this way as well expected += "/" assert client.get_cachedir(cachedir, master=master) == expected @pytest.mark.usefixtures("_setup") @pytest.mark.parametrize("saltenv", _saltenvs()) def test_cache_file_send_fail(client, saltenv, caplog, cache_root, fs_root): """ Ensure that when a file transfer fails, an error is logged and the locally cached file is removed """ with caplog.at_level(logging.ERROR): assert not client.cache_file(f"salt://{SUBDIR}/fail.sls", saltenv) assert "Failed sending file: sth went wrong" in caplog.text cache_loc = ( Path(cache_root) / "salt-ssh" / TGT / "files" / saltenv / SUBDIR / "fail.sls" ) assert not cache_loc.exists() @pytest.mark.usefixtures("_setup") def test_cache_file_dir_to_file(client, cache_root, remote_list, caplog): """ Ensure that when a remote path is a directory, but should be a file, cache_file will remove the directory. """ local_cache = Path(cache_root) / "salt-ssh" / TGT / "files" / "base" / "dirtofile" expected = Path(cache_root) / "files" / "base" / "dirtofile" res = client.cache_file("salt://dirtofile") assert res assert "just copied a file into a dir" not in caplog.text assert res == str(local_cache) assert local_cache.exists() assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == str(expected) client.shell.send.assert_called_once_with(str(local_cache), str(expected), True) @pytest.mark.usefixtures("_setup") def test_cache_file_file_to_dir(client, cache_root, caplog): """ Ensure that when a remote path is a file, but should be a directory, cache_file will remove the file. """ local_cache = ( Path(cache_root) / "salt-ssh" / TGT / "files" / "base" / "filetodir" / "foo.sh" ) expected = Path(cache_root) / "files" / "base" / "filetodir" / "foo.sh" res = client.cache_file("salt://filetodir/foo.sh") assert res assert "path contains files which were not removed" not in caplog.text assert res == str(local_cache) assert local_cache.exists() assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == str(expected) client.shell.send.assert_called_with(str(local_cache), str(expected), True) @pytest.mark.usefixtures("_setup") def test_send_file_to_dir_rmfail(client, cache_root, caplog): """ Ensure that when a remote path is a file, but should be a directory, cache_file will remove the file. """ res = client._send_file( "/some/source/file", Path(cache_root) / "files/base/rmfail/rmfailfile/child", True, None, ) assert res is False assert "path contains files which were not removed" in caplog.text assert "Failed deleting path" in caplog.text assert "you shall not pass" in caplog.text def test_send_file_file_to_dir_outside_cachedir(client, caplog): """ When sending a file to a path of which one parent is an existing file, ensure that the failure is passed through instead of trying to delete this file. """ res = client._send_file( "/some/source/file", "/tmp/targetdir/existingfile/bar", True, None ) assert res is False assert "path contains files which were not removed" in caplog.text assert ( call("rm -rf /tmp/targetdir/existingfile") not in client.shell.exec_cmd.mock_calls ) def test_send_file_from_outside_cachedir_fail(client, caplog, tmp_path): """ Ensure failed transfers do not result in a deleted source file if it is outside of the SSH master-minion cache """ src = tmp_path / "this_should_not_be_deleted" src.touch() with caplog.at_level(logging.ERROR): res = client._send_file( "/some/source/file", "/tmp/targetdir/failfile", False, None ) assert res is False assert src.exists() assert "sth went wrong" in caplog.text @pytest.mark.usefixtures("_setup") @pytest.mark.parametrize("saltenv", _saltenvs()) def test_get_file_to_cache_send_fail(client, saltenv, caplog, cache_root, fs_root): """ Ensure that when a file transfer to the remote cache fails, an error is logged and the locally cached file is removed """ with caplog.at_level(logging.ERROR): assert not client.get_file(f"salt://{SUBDIR}/fail.sls", "", saltenv=saltenv) assert "Failed sending file: sth went wrong" in caplog.text cache_loc = ( Path(cache_root) / "salt-ssh" / TGT / "files" / saltenv / SUBDIR / "fail.sls" ) assert not cache_loc.exists() @pytest.mark.usefixtures("_setup") @pytest.mark.parametrize("saltenv", _saltenvs()) def test_get_file_send_fail_dst(client, saltenv, caplog, cache_root, fs_root, tmp_path): """ Ensure that when a file transfer to a remote path outside of the cachedir fails, an error is logged and the locally cached file is removed """ tgt = tmp_path / "remote_target_fail" with caplog.at_level(logging.ERROR): assert not client.get_file(f"salt://{SUBDIR}/fail.sls", str(tgt), saltenv) assert not tgt.exists() assert "Failed sending file: sth went wrong" in caplog.text cache_loc = ( Path(cache_root) / "salt-ssh" / TGT / "files" / saltenv / SUBDIR / "fail.sls" ) assert not cache_loc.exists() @pytest.mark.usefixtures("_setup") @pytest.mark.parametrize("makedirs", (False, True)) def test_get_file_remote_isdir(client, cache_root, makedirs): """ Ensure that if the remote path is a directory, the file will be copied into it, _send will be called with the full file path and that the correct path is present in the target map. """ tgt = "/tmp/targetdir" expected = tgt + "/foo.txt" local_cache = Path(cache_root) / "salt-ssh" / TGT / "files" / "base" / "foo.txt" res = client.get_file("salt://foo.txt", tgt, makedirs=makedirs) assert res assert res == str(local_cache) assert local_cache.exists() assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == expected client.shell.send.assert_called_once_with(str(local_cache), expected, makedirs) @pytest.mark.usefixtures("_setup") @pytest.mark.parametrize("makedirs", (False, True)) def test_get_file_with_dest_slash(client, cache_root, makedirs): """ Ensure that if the destination is specified with a trailing slash, it is assumed to be a directory and a check is not performed. """ tgt = "/tmp/targetdir/" expected = tgt + "foo.txt" local_cache = Path(cache_root) / "salt-ssh" / TGT / "files" / "base" / "foo.txt" res = client.get_file("salt://foo.txt", tgt, makedirs=makedirs) assert res assert res == str(local_cache) assert local_cache.exists() assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == expected client.shell.send.assert_called_once_with(str(local_cache), expected, makedirs) # ensure we're not doing unnecessary remote executions assert call(f"test -d {tgt}") not in client.shell.exec_cmd.mock_calls @pytest.mark.usefixtures("_setup") def test_get_file_with_different_name(client, cache_root): """ Ensure that if the target path is not a directory, the final path component will be the name of the file. """ tgt = "/tmp/targetdir/hithere" local_cache = Path(cache_root) / "salt-ssh" / TGT / "files" / "base" / "foo.txt" res = client.get_file("salt://foo.txt", tgt) assert res assert res == str(local_cache) assert local_cache.exists() assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == tgt client.shell.send.assert_called_once_with(str(local_cache), tgt, False) @pytest.mark.usefixtures("_setup") def test_get_url_salt_none_without_no_cache(client, cache_root): """ note: salt:// URIs do not respect no_cache """ tgt = Path(cache_root) / "files" / "base" / "foo.txt" local_cache = Path(cache_root) / "salt-ssh" / TGT / "files" / "base" / "foo.txt" res = client.get_url("salt://foo.txt", None) assert res assert local_cache.exists() assert res == local_cache.read_bytes() assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == str(tgt) client.shell.send.assert_called_once_with(str(local_cache), str(tgt), True) @pytest.fixture def _http_patch(): def query(*args, **kwargs): kwargs["header_callback"]("HTTP/1.1 200 OK") kwargs["streaming_callback"](b"hi there") return {"handle": Mock(), "status": 200} with patch("salt.utils.http.query", side_effect=query): yield @pytest.mark.usefixtures("_setup", "_http_patch") @pytest.mark.parametrize("dest", ("", None)) def test_get_url_non_salt_dest_empty_without_no_cache(client, cache_root, dest): """ Even when dest is None, but no_cache is False, the file should be sent to the minion cache. """ tgt = Path(cache_root) / "extrn_files" / "base" / "saltproject.io" / "index.html" local_cache = ( Path(cache_root) / "salt-ssh" / TGT / "extrn_files" / "base" / "saltproject.io" / "index.html" ) res = client.get_url("https://saltproject.io/index.html", dest) assert res assert local_cache.exists() assert res == str(local_cache) assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == str(tgt) client.shell.send.assert_called_once_with(str(local_cache), str(tgt), True) @pytest.mark.usefixtures("_setup", "_http_patch") def test_get_url_non_salt_dest_slash(client, cache_root, tmp_path): expected = str(tmp_path) + "/foo.html" local_cache = ( Path(cache_root) / "salt-ssh" / TGT / "extrn_files" / "base" / "saltproject.io" / "foo.html" ) res = client.get_url("https://saltproject.io/foo.html", str(tmp_path) + "/") assert res assert local_cache.exists() assert local_cache.read_text() == "hi there" assert res == str(local_cache) assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == expected client.shell.send.assert_called_once_with(str(local_cache), expected, False) assert call(f"test -d {tmp_path}") not in client.shell.exec_cmd.mock_calls @pytest.mark.usefixtures("_setup", "_http_patch") def test_get_url_non_salt_dest_isdir(client, cache_root): dest = "/tmp/targetdir" expected = dest + "/foo.html" local_cache = ( Path(cache_root) / "salt-ssh" / TGT / "extrn_files" / "base" / "saltproject.io" / "foo.html" ) res = client.get_url("https://saltproject.io/foo.html", dest) assert res assert local_cache.exists() assert local_cache.read_text() == "hi there" assert res == str(local_cache) assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == expected client.shell.send.assert_called_once_with(str(local_cache), expected, False) client.shell.exec_cmd.assert_called_with(f"test -d {dest}") @pytest.mark.usefixtures("_setup", "_http_patch") def test_get_url_non_salt_dest_name_override(client, cache_root): dest = "/tmp/targetdir/bar.html" local_cache = ( Path(cache_root) / "salt-ssh" / TGT / "extrn_files" / "base" / "saltproject.io" / "foo.html" ) res = client.get_url("https://saltproject.io/foo.html", dest) assert res assert local_cache.exists() assert res == str(local_cache) assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == dest client.shell.send.assert_called_once_with(str(local_cache), dest, False) client.shell.exec_cmd.assert_called_with(f"test -d {dest}") @pytest.mark.usefixtures("_setup", "_http_patch") def test_get_url_non_salt_dest_default_name(client, cache_root, tmp_path): expected = str(tmp_path / "index.html") # Yup, the regular fileclient will save this with the domain name only. # If you then try to cache any other file from that domain, it will # actually raise an exception because it attempts to create a dir with the same name local_cache = ( Path(cache_root) / "salt-ssh" / TGT / "extrn_files" / "base" / "saltproject.io" ) res = client.get_url("https://saltproject.io", str(tmp_path) + "/") assert res assert local_cache.exists() assert local_cache.read_text() == "hi there" assert res == str(local_cache) assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == expected client.shell.send.assert_called_once_with(str(local_cache), expected, False) def test_get_url_non_salt_fetch_fail(client): with patch("salt.fileclient.Client.get_url", return_value=False): res = client.get_url("https://fooba.rs", "") assert res is False @pytest.mark.usefixtures("_setup") @pytest.mark.parametrize("dest", ("", None)) def test_get_template_cache(client, dest, cache_root): tgt = Path(cache_root) / "extrn_files" / "base" / "foo.txt" local_cache = ( Path(cache_root) / "salt-ssh" / TGT / "extrn_files" / "base" / "foo.txt" ) res = client.get_template("salt://foo.txt", dest, opts=client.opts) assert res assert local_cache.exists() assert res == str(local_cache) assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == str(tgt) client.shell.send.assert_called_with(str(local_cache), str(tgt), True) @pytest.mark.usefixtures("_setup") def test_get_template_name_override(client, cache_root): dest = "/tmp/targetdir/bar" local_cache = ( Path(cache_root) / "salt-ssh" / TGT / "extrn_files" / "base" / "foo.txt" ) res = client.get_template("salt://foo.txt", dest, opts=client.opts) assert res assert local_cache.exists() assert res == str(local_cache) assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == dest client.shell.send.assert_called_with(str(local_cache), dest, False) @pytest.mark.usefixtures("_setup", "_http_patch") def test_get_template_dest_slash(client, cache_root, tmp_path): expected = str(tmp_path) + "/foo.txt" local_cache = ( Path(cache_root) / "salt-ssh" / TGT / "extrn_files" / "base" / "foo.txt" ) res = client.get_template("salt://foo.txt", str(tmp_path) + "/", opts=client.opts) assert res assert local_cache.exists() assert ( local_cache.read_text().strip() == "This is a test file in the 'base' saltenv." ) assert res == str(local_cache) assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == expected client.shell.send.assert_called_with(str(local_cache), expected, False) assert call(f"test -d {tmp_path}") not in client.shell.exec_cmd.mock_calls @pytest.mark.usefixtures("_setup", "_http_patch") def test_get_template_dest_isdir(client, cache_root): dest = "/tmp/targetdir" expected = dest + "/foo.txt" local_cache = ( Path(cache_root) / "salt-ssh" / TGT / "extrn_files" / "base" / "foo.txt" ) res = client.get_template("salt://foo.txt", dest, opts=client.opts) assert res assert local_cache.exists() assert ( local_cache.read_text().strip() == "This is a test file in the 'base' saltenv." ) assert res == str(local_cache) assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == expected client.shell.send.assert_called_with(str(local_cache), expected, False) client.shell.exec_cmd.assert_called_with(f"test -d {dest}") @pytest.mark.usefixtures("_setup", "_http_patch") def test_get_template_dest_name_override(client, cache_root): dest = "/tmp/targetdir/bar.html" local_cache = ( Path(cache_root) / "salt-ssh" / TGT / "extrn_files" / "base" / "saltproject.io" / "foo.html" ) res = client.get_url("https://saltproject.io/foo.html", dest) assert res assert local_cache.exists() assert local_cache.read_text() == "hi there" assert res == str(local_cache) assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == dest client.shell.send.assert_called_once_with(str(local_cache), dest, False) client.shell.exec_cmd.assert_called_with(f"test -d {dest}") @pytest.mark.usefixtures("_setup") def test_get_template_dir_to_file(client, cache_root, remote_list, caplog): """ Ensure that when a remote path is a directory, but should be a file, get_template will remove the directory. """ local_cache = ( Path(cache_root) / "salt-ssh" / TGT / "extrn_files" / "base" / "dirtofile" ) expected = Path(cache_root) / "extrn_files" / "base" / "dirtofile" res = client.get_template("salt://dirtofile", "", opts=client.opts) assert res assert "just copied a file into a dir" not in caplog.text assert res == str(local_cache) assert local_cache.exists() assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == str(expected) client.shell.send.assert_called_with(str(local_cache), str(expected), True) @pytest.mark.usefixtures("_setup") def test_get_template_file_to_dir(client, cache_root, caplog): """ Ensure that when a remote path is a file, but should be a directory, get_template will remove the file. """ local_cache = ( Path(cache_root) / "salt-ssh" / TGT / "extrn_files" / "base" / "filetodir" / "foo.sh" ) expected = Path(cache_root) / "extrn_files" / "base" / "filetodir" / "foo.sh" res = client.get_template("salt://filetodir/foo.sh", "", opts=client.opts) assert res assert "path contains files which were not removed" not in caplog.text assert res == str(local_cache) assert local_cache.exists() assert str(local_cache) in client.target_map assert client.target_map[str(local_cache)] == str(expected) client.shell.send.assert_called_with(str(local_cache), str(expected), True) @pytest.mark.parametrize( "arg,raises", [ ( ("foo/bar", "/foo/bar"), pytest.raises(ValueError, match=".*must be absolute.*as src"), ), ( ("/foo/bar", "foo/bar"), pytest.raises(ValueError, match=".*must be absolute.*as dest"), ), ], ) def test_send_file_sanity_checks(client, raises, arg): with raises: client._send_file(*arg, True, None) @pytest.mark.parametrize( "path,raises", [ ( "", pytest.raises( ValueError, match="Not deleting unspecified, relative or root path" ), ), ( "foo/bar", pytest.raises( ValueError, match="Not deleting unspecified, relative or root path" ), ), ( "/", pytest.raises( ValueError, match="Not deleting unspecified, relative or root path" ), ), ( "/some/path/not/in/cache/dir", pytest.raises( ValueError, match="Not recursively deleting a path outside of the cachedir.*", ), ), ], ) def test_rmpath_sanity_checks(client, raises, path): with raises: client._rmpath(path) client.shell.exec_cmd.assert_not_called() def test_rmpath_can_delete_minion_cache_dir(client, cache_root): client._rmpath(cache_root) client.shell.exec_cmd.assert_called_once_with(f"rm -rf {cache_root}") def test_rmpath_does_not_delete_minion_cache_dir_parent(client, cache_root): with pytest.raises( ValueError, match="Not recursively deleting a path outside of the cachedir.*" ): client._rmpath(Path(cache_root).parent) client.shell.exec_cmd.assert_not_called() @pytest.mark.parametrize( "path,expected", [("files/base/dirtofile", True), ("files/base/rmfail", False)] ) def test_rmpath_retcode(client, cache_root, path, expected, caplog): with caplog.at_level(logging.ERROR): res = client._rmpath(Path(cache_root) / path) assert res is expected if not expected: assert "Failed deleting path" in caplog.text assert "you shall not pass" in caplog.text def test_is_cached_localfiles(client, cache_root): """ A check for having cached a local file on the minion should only check if the file exists on the target since we're not syncing to the master """ tgt = "/this/file/was/cached/locally" res = client.is_cached(tgt) assert res == str(Path(cache_root) / "localfiles" / tgt[1:]) @pytest.mark.parametrize("saltenv", _saltenvs()) @pytest.mark.usefixtures("_setup") def test_cache_master(client, cache_root, saltenv): """ Files are copied one by one currently, so this is done as a unit test to save execution time and resources. """ with patch.object(cp, "_client", return_value=client): res = cp.cache_master(saltenv) assert isinstance(res, list) assert res minion_parent = Path(cache_root) / "files" / saltenv master_parent = Path(cache_root) / "salt-ssh" / TGT / "files" / saltenv for path in res: assert path path = Path(path) assert minion_parent in path.parents master_path = master_parent / path.relative_to(minion_parent) assert master_path.exists() if path.name == "foo.txt": assert saltenv in master_path.read_text() client.shell.send.assert_any_call(str(master_path), str(path), True)