salt/tests/integration/modules/test_file.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

395 lines
15 KiB
Python
Raw Normal View History

import getpass
import os
import shutil
import sys
import pytest
import salt.utils.files
Use explicit unicode strings + break up salt.utils This PR is part of what will be an ongoing effort to use explicit unicode strings in Salt. Because Python 3 does not suport Python 2's raw unicode string syntax (i.e. `ur'\d+'`), we must use `salt.utils.locales.sdecode()` to ensure that the raw string is unicode. However, because of how `salt/utils/__init__.py` has evolved into the hulking monstrosity it is today, this means importing a large module in places where it is not needed, which could negatively impact performance. For this reason, this PR also breaks out some of the functions from `salt/utils/__init__.py` into new/existing modules under `salt/utils/`. The long term goal will be that the modules within this directory do not depend on importing `salt.utils`. A summary of the changes in this PR is as follows: * Moves the following functions from `salt.utils` to new locations (including a deprecation warning if invoked from `salt.utils`): `to_bytes`, `to_str`, `to_unicode`, `str_to_num`, `is_quoted`, `dequote`, `is_hex`, `is_bin_str`, `rand_string`, `contains_whitespace`, `clean_kwargs`, `invalid_kwargs`, `which`, `which_bin`, `path_join`, `shlex_split`, `rand_str`, `is_windows`, `is_proxy`, `is_linux`, `is_darwin`, `is_sunos`, `is_smartos`, `is_smartos_globalzone`, `is_smartos_zone`, `is_freebsd`, `is_netbsd`, `is_openbsd`, `is_aix` * Moves the functions already deprecated by @rallytime to the bottom of `salt/utils/__init__.py` for better organization, so we can keep the deprecated ones separate from the ones yet to be deprecated as we continue to break up `salt.utils` * Updates `salt/*.py` and all files under `salt/client/` to use explicit unicode string literals. * Gets rid of implicit imports of `salt.utils` (e.g. `from salt.utils import foo` becomes `import salt.utils.foo as foo`). * Renames the `test.rand_str` function to `test.random_hash` to more accurately reflect what it does * Modifies `salt.utils.stringutils.random()` (née `salt.utils.rand_string()`) such that it returns a string matching the passed size. Previously this function would get `size` bytes from `os.urandom()`, base64-encode it, and return the result, which would in most cases not be equal to the passed size.
2017-07-24 20:47:15 -05:00
import salt.utils.platform
2025-01-16 15:17:17 -07:00
import salt.utils.stringutils
from tests.support.case import ModuleCase
from tests.support.helpers import requires_system_grains
2018-12-07 17:52:49 +00:00
from tests.support.runtests import RUNTIME_VARS
2020-04-02 20:10:20 -05:00
# Posix only
try:
import grp
import pwd
except ImportError:
pass
# Windows only
try:
import win32file
except ImportError:
pass
def symlink(source, link_name):
2020-04-02 20:10:20 -05:00
"""
Handle symlinks on Windows with Python < 3.2
2020-04-02 20:10:20 -05:00
"""
if salt.utils.platform.is_windows():
win32file.CreateSymbolicLink(link_name, source)
else:
os.symlink(source, link_name)
@pytest.mark.windows_whitelisted
class FileModuleTest(ModuleCase):
2020-04-02 20:10:20 -05:00
"""
Validate the file module
2020-04-02 20:10:20 -05:00
"""
def setUp(self):
2018-12-07 17:52:49 +00:00
self.myfile = os.path.join(RUNTIME_VARS.TMP, "myfile")
with salt.utils.files.fopen(self.myfile, "w+") as fp:
fp.write(salt.utils.stringutils.to_str("Hello" + os.linesep))
2018-12-07 17:52:49 +00:00
self.mydir = os.path.join(RUNTIME_VARS.TMP, "mydir/isawesome")
if not os.path.isdir(self.mydir):
# left behind... Don't fail because of this!
os.makedirs(self.mydir)
2018-12-07 17:52:49 +00:00
self.mysymlink = os.path.join(RUNTIME_VARS.TMP, "mysymlink")
if os.path.islink(self.mysymlink) or os.path.isfile(self.mysymlink):
os.remove(self.mysymlink)
symlink(self.myfile, self.mysymlink)
2018-12-07 17:52:49 +00:00
self.mybadsymlink = os.path.join(RUNTIME_VARS.TMP, "mybadsymlink")
if os.path.islink(self.mybadsymlink) or os.path.isfile(self.mybadsymlink):
os.remove(self.mybadsymlink)
symlink("/nonexistentpath", self.mybadsymlink)
super().setUp()
def tearDown(self):
if os.path.isfile(self.myfile):
os.remove(self.myfile)
if os.path.islink(self.mysymlink) or os.path.isfile(self.mysymlink):
os.remove(self.mysymlink)
if os.path.islink(self.mybadsymlink) or os.path.isfile(self.mybadsymlink):
os.remove(self.mybadsymlink)
shutil.rmtree(self.mydir, ignore_errors=True)
super().tearDown()
@pytest.mark.skip_on_windows(reason="No security context on Windows")
@requires_system_grains
def test_get_selinux_context(self, grains):
if grains.get("selinux", {}).get("enabled", False):
NEW_CONTEXT = "system_u:object_r:system_conf_t:s0"
self.run_function(
"file.set_selinux_context", arg=[self.myfile, *(NEW_CONTEXT.split(":"))]
)
ret_file = self.run_function("file.get_selinux_context", arg=[self.myfile])
self.assertEqual(ret_file, NEW_CONTEXT)
# Issue #56557. Ensure that the context of the directory
# containing one file is the context of the directory itself, and
# not the context of the first file in the directory.
self.run_function(
"file.set_selinux_context", arg=[self.mydir, *(NEW_CONTEXT.split(":"))]
)
ret_dir = self.run_function("file.get_selinux_context", arg=[self.mydir])
self.assertEqual(ret_dir, NEW_CONTEXT)
ret_updir = self.run_function(
"file.get_selinux_context",
arg=[os.path.abspath(os.path.join(self.mydir, ".."))],
)
self.assertNotEqual(ret_updir, NEW_CONTEXT)
else:
ret_file = self.run_function("file.get_selinux_context", arg=[self.myfile])
self.assertIn("No selinux context information is available", ret_file)
@pytest.mark.skip_on_windows(reason="No security context on Windows")
@requires_system_grains
def test_set_selinux_context(self, grains):
if not grains.get("selinux", {}).get("enabled", False):
self.skipTest("selinux not available")
FILE_CONTEXT = "system_u:object_r:system_conf_t:s0"
ret_file = self.run_function(
"file.set_selinux_context", arg=[self.myfile, *(FILE_CONTEXT.split(":"))]
)
self.assertEqual(ret_file, FILE_CONTEXT)
DIR_CONTEXT = "system_u:object_r:user_home_t:s0"
ret_dir = self.run_function(
"file.set_selinux_context", arg=[self.mydir, *(DIR_CONTEXT.split(":"))]
)
self.assertEqual(ret_dir, DIR_CONTEXT)
@pytest.mark.skip_on_windows(reason="No chgrp on Windows")
def test_chown(self):
user = getpass.getuser()
if sys.platform == "darwin":
group = "staff"
2014-09-04 10:23:59 +02:00
elif sys.platform.startswith(("linux", "freebsd", "openbsd")):
group = grp.getgrgid(pwd.getpwuid(os.getuid()).pw_gid).gr_name
ret = self.run_function("file.chown", arg=[self.myfile, user, group])
self.assertIsNone(ret)
fstat = os.stat(self.myfile)
self.assertEqual(fstat.st_uid, os.getuid())
self.assertEqual(fstat.st_gid, grp.getgrnam(group).gr_gid)
@pytest.mark.skip_on_windows(reason="No chgrp on Windows")
def test_chown_no_user(self):
user = "notanyuseriknow"
group = grp.getgrgid(pwd.getpwuid(os.getuid()).pw_gid).gr_name
ret = self.run_function("file.chown", arg=[self.myfile, user, group])
self.assertIn("not exist", ret)
@pytest.mark.skip_on_windows(reason="No chgrp on Windows")
def test_chown_no_user_no_group(self):
user = "notanyuseriknow"
group = "notanygroupyoushoulduse"
ret = self.run_function("file.chown", arg=[self.myfile, user, group])
self.assertIn("Group does not exist", ret)
self.assertIn("User does not exist", ret)
@pytest.mark.skip_on_windows(reason="No chgrp on Windows")
def test_chown_no_path(self):
user = getpass.getuser()
if sys.platform == "darwin":
group = "staff"
2014-09-04 10:23:59 +02:00
elif sys.platform.startswith(("linux", "freebsd", "openbsd")):
group = grp.getgrgid(pwd.getpwuid(os.getuid()).pw_gid).gr_name
ret = self.run_function("file.chown", arg=["/tmp/nosuchfile", user, group])
self.assertIn("File not found", ret)
@pytest.mark.skip_on_windows(reason="No chgrp on Windows")
def test_chown_noop(self):
user = ""
group = ""
ret = self.run_function("file.chown", arg=[self.myfile, user, group])
self.assertIsNone(ret)
fstat = os.stat(self.myfile)
self.assertEqual(fstat.st_uid, os.getuid())
self.assertEqual(fstat.st_gid, os.getgid())
@pytest.mark.skip_on_windows(reason="No chgrp on Windows")
def test_chgrp(self):
if sys.platform == "darwin":
group = "everyone"
2014-09-04 10:23:59 +02:00
elif sys.platform.startswith(("linux", "freebsd", "openbsd")):
group = grp.getgrgid(pwd.getpwuid(os.getuid()).pw_gid).gr_name
ret = self.run_function("file.chgrp", arg=[self.myfile, group])
self.assertIsNone(ret)
fstat = os.stat(self.myfile)
self.assertEqual(fstat.st_gid, grp.getgrnam(group).gr_gid)
@pytest.mark.skip_on_windows(reason="No chgrp on Windows")
def test_chgrp_failure(self):
group = "thisgroupdoesntexist"
ret = self.run_function("file.chgrp", arg=[self.myfile, group])
self.assertIn("not exist", ret)
2012-10-17 14:06:17 -04:00
def test_patch(self):
if not self.run_function("cmd.has_exec", ["patch"]):
self.skipTest("patch is not installed")
2018-12-07 17:52:49 +00:00
src_patch = os.path.join(RUNTIME_VARS.FILES, "file", "base", "hello.patch")
src_file = os.path.join(RUNTIME_VARS.TMP, "src.txt")
with salt.utils.files.fopen(src_file, "w+") as fp:
fp.write(salt.utils.stringutils.to_str("Hello\n"))
2012-10-17 14:06:17 -04:00
# dry-run should not modify src_file
ret = self.minion_run("file.patch", src_file, src_patch, dry_run=True)
assert ret["retcode"] == 0, repr(ret)
with salt.utils.files.fopen(src_file) as fp:
self.assertEqual(salt.utils.stringutils.to_unicode(fp.read()), "Hello\n")
2012-10-17 14:06:17 -04:00
ret = self.minion_run("file.patch", src_file, src_patch)
assert ret["retcode"] == 0, repr(ret)
with salt.utils.files.fopen(src_file) as fp:
self.assertEqual(
2025-01-16 15:17:17 -07:00
salt.utils.stringutils.to_unicode(fp.read()), f"Hello world{os.linesep}"
)
2012-10-17 14:06:17 -04:00
def test_remove_file(self):
ret = self.run_function("file.remove", arg=[self.myfile])
self.assertTrue(ret)
def test_remove_dir(self):
ret = self.run_function("file.remove", arg=[self.mydir])
self.assertTrue(ret)
def test_remove_symlink(self):
ret = self.run_function("file.remove", arg=[self.mysymlink])
self.assertTrue(ret)
def test_remove_broken_symlink(self):
ret = self.run_function("file.remove", arg=[self.mybadsymlink])
self.assertTrue(ret)
def test_cannot_remove(self):
ret = self.run_function("file.remove", arg=["tty"])
self.assertEqual(
"ERROR executing 'file.remove': File path must be absolute: tty", ret
)
def test_source_list_for_single_file_returns_unchanged(self):
ret = self.run_function(
"file.source_list", ["salt://http/httpd.conf", "filehash", "base"]
)
Python 3 Fixes (Pt. 2) (#39397) * Typo in comment * First convert to string if not already a string. Then to bytes under Py3. The reason being that jids from the CLI, at least the one fed in integration.runners.jobs.ManageTest.test_loopup_jid is loaded as an integer, and, while the Py2 code converts JIDs to strings, under Py3, we assume JID's are already strings. * Mark tests which require root permissions to run * Allow declaring that the function IS a class method. ``` Python 3.5.3 (default, Jan 21 2017, 00:29:12) [GCC 6.3.1 20170109] on linux Type "help", "copyright", "credits" or "license" for more information. >>> class Foo: ... def bar(self): ... print('bar') ... >>> import inspect >>> inspect.ismethod(Foo.bar) False >>> inspect.ismethod(Foo().bar) True ``` On Python 2, `inspect.ismethod` returns `True` for bound and unbound methods while on Python 3 it only returns `True` for bound methods. The explicit `is_class_method` is to avoid instantiating the class just to get the function signature. * Always decode responses to the Python version native string implementation * Just compare the objects as matching list. Asserting same item count doesn't make that much sense. * Py3 compatibility * Fix saltnado tests under Py3 * Python 3 compatibility * Show me the full traceback * Revert "Convert fileserver data from bytes to strings" This reverts commit e53972f8c6464c0fdeffb875c785b3ce530c7c5e. * Revert "Under Py3, we get `bytes` when using the roots backend directly" This reverts commit 9f73b240c1d68d785a04d898fab84837c154a5ae. * Convert from bytes to str if not a binary file * Py3 compatibility fixes. Convert file contents from bytes to string if not a binary file
2017-02-14 23:20:56 +00:00
self.assertEqual(list(ret), ["salt://http/httpd.conf", "filehash"])
def test_source_list_for_single_local_file_slash_returns_unchanged(self):
ret = self.run_function("file.source_list", [self.myfile, "filehash", "base"])
Python 3 Fixes (Pt. 2) (#39397) * Typo in comment * First convert to string if not already a string. Then to bytes under Py3. The reason being that jids from the CLI, at least the one fed in integration.runners.jobs.ManageTest.test_loopup_jid is loaded as an integer, and, while the Py2 code converts JIDs to strings, under Py3, we assume JID's are already strings. * Mark tests which require root permissions to run * Allow declaring that the function IS a class method. ``` Python 3.5.3 (default, Jan 21 2017, 00:29:12) [GCC 6.3.1 20170109] on linux Type "help", "copyright", "credits" or "license" for more information. >>> class Foo: ... def bar(self): ... print('bar') ... >>> import inspect >>> inspect.ismethod(Foo.bar) False >>> inspect.ismethod(Foo().bar) True ``` On Python 2, `inspect.ismethod` returns `True` for bound and unbound methods while on Python 3 it only returns `True` for bound methods. The explicit `is_class_method` is to avoid instantiating the class just to get the function signature. * Always decode responses to the Python version native string implementation * Just compare the objects as matching list. Asserting same item count doesn't make that much sense. * Py3 compatibility * Fix saltnado tests under Py3 * Python 3 compatibility * Show me the full traceback * Revert "Convert fileserver data from bytes to strings" This reverts commit e53972f8c6464c0fdeffb875c785b3ce530c7c5e. * Revert "Under Py3, we get `bytes` when using the roots backend directly" This reverts commit 9f73b240c1d68d785a04d898fab84837c154a5ae. * Convert from bytes to str if not a binary file * Py3 compatibility fixes. Convert file contents from bytes to string if not a binary file
2017-02-14 23:20:56 +00:00
self.assertEqual(list(ret), [self.myfile, "filehash"])
def test_source_list_for_single_local_file_proto_returns_unchanged(self):
ret = self.run_function(
"file.source_list", ["file://" + self.myfile, "filehash", "base"]
)
Python 3 Fixes (Pt. 2) (#39397) * Typo in comment * First convert to string if not already a string. Then to bytes under Py3. The reason being that jids from the CLI, at least the one fed in integration.runners.jobs.ManageTest.test_loopup_jid is loaded as an integer, and, while the Py2 code converts JIDs to strings, under Py3, we assume JID's are already strings. * Mark tests which require root permissions to run * Allow declaring that the function IS a class method. ``` Python 3.5.3 (default, Jan 21 2017, 00:29:12) [GCC 6.3.1 20170109] on linux Type "help", "copyright", "credits" or "license" for more information. >>> class Foo: ... def bar(self): ... print('bar') ... >>> import inspect >>> inspect.ismethod(Foo.bar) False >>> inspect.ismethod(Foo().bar) True ``` On Python 2, `inspect.ismethod` returns `True` for bound and unbound methods while on Python 3 it only returns `True` for bound methods. The explicit `is_class_method` is to avoid instantiating the class just to get the function signature. * Always decode responses to the Python version native string implementation * Just compare the objects as matching list. Asserting same item count doesn't make that much sense. * Py3 compatibility * Fix saltnado tests under Py3 * Python 3 compatibility * Show me the full traceback * Revert "Convert fileserver data from bytes to strings" This reverts commit e53972f8c6464c0fdeffb875c785b3ce530c7c5e. * Revert "Under Py3, we get `bytes` when using the roots backend directly" This reverts commit 9f73b240c1d68d785a04d898fab84837c154a5ae. * Convert from bytes to str if not a binary file * Py3 compatibility fixes. Convert file contents from bytes to string if not a binary file
2017-02-14 23:20:56 +00:00
self.assertEqual(list(ret), ["file://" + self.myfile, "filehash"])
def test_source_list_for_multiple_files_with_missing_files(self):
file_list = [
"salt://does/not/exist",
"file://" + self.myfile,
"http://localhost//does/not/exist",
"salt://http/httpd.conf",
]
ret = self.run_function("file.source_list", [file_list, "filehash", "base"])
self.assertEqual(list(ret), ["file://" + self.myfile, "filehash"])
def test_source_list_for_multiple_files_dict_with_missing_files(self):
file_list = [
{"salt://does/not/exist": "filehash"},
{"file://" + self.myfile: "filehash"},
{"http://localhost//does/not/exist": "filehash"},
{"salt://http/httpd.conf": "filehash"},
]
ret = self.run_function("file.source_list", [file_list, "", "base"])
self.assertEqual(list(ret), ["file://" + self.myfile, "filehash"])
def test_file_line_changes_format(self):
2020-04-02 20:10:20 -05:00
"""
Test file.line changes output formatting.
Issue #41474
2020-04-02 20:10:20 -05:00
"""
ret = self.minion_run(
"file.line", self.myfile, "Goodbye", mode="insert", after="Hello"
)
2017-11-07 00:30:48 -08:00
self.assertIn("Hello" + os.linesep + "+Goodbye", ret)
def test_file_line_changes_entire_line(self):
"""
Test file.line entire line matching
Issue #49855
"""
ret = self.minion_run(
"file.line", self.myfile, "Goodbye", mode="insert", after="Hello"
)
assert "Hello" + os.linesep + "+Goodbye" in ret
ret = self.minion_run(
"file.line", self.myfile, "Goodbye 1", mode="insert", after="Hello"
)
assert (
"Hello" + os.linesep + "+Goodbye 1" + os.linesep + " Goodbye" + os.linesep
in ret
)
with salt.utils.files.fopen(self.myfile, "r") as fh_:
content = fh_.read()
assert (
"Hello" + os.linesep + "Goodbye 1" + os.linesep + "Goodbye" + os.linesep
== content
)
def test_file_line_content(self):
self.minion_run(
"file.line", self.myfile, "Goodbye", mode="insert", after="Hello"
)
with salt.utils.files.fopen(self.myfile, "r") as fp:
content = fp.read()
self.assertEqual(content, "Hello" + os.linesep + "Goodbye" + os.linesep)
2018-11-02 23:02:04 +01:00
def test_file_line_duplicate_insert_after(self):
"""
Test file.line duplicates line.
Issue #50254
"""
with salt.utils.files.fopen(self.myfile, "a") as fp:
fp.write(salt.utils.stringutils.to_str("Goodbye" + os.linesep))
self.minion_run(
"file.line", self.myfile, "Goodbye", mode="insert", after="Hello"
)
with salt.utils.files.fopen(self.myfile, "r") as fp:
content = fp.read()
self.assertEqual(content, "Hello" + os.linesep + "Goodbye" + os.linesep)
def test_file_line_duplicate_insert_before(self):
"""
Test file.line duplicates line.
Issue #50254
"""
with salt.utils.files.fopen(self.myfile, "a") as fp:
fp.write(salt.utils.stringutils.to_str("Goodbye" + os.linesep))
self.minion_run(
"file.line", self.myfile, "Hello", mode="insert", before="Goodbye"
)
with salt.utils.files.fopen(self.myfile, "r") as fp:
content = fp.read()
self.assertEqual(content, "Hello" + os.linesep + "Goodbye" + os.linesep)
def test_file_line_duplicate_ensure_after(self):
"""
Test file.line duplicates line.
Issue #50254
"""
with salt.utils.files.fopen(self.myfile, "a") as fp:
fp.write(salt.utils.stringutils.to_str("Goodbye" + os.linesep))
self.minion_run(
"file.line", self.myfile, "Goodbye", mode="ensure", after="Hello"
)
with salt.utils.files.fopen(self.myfile, "r") as fp:
content = fp.read()
self.assertEqual(content, "Hello" + os.linesep + "Goodbye" + os.linesep)
def test_file_line_duplicate_ensure_before(self):
"""
Test file.line duplicates line.
Issue #50254
"""
with salt.utils.files.fopen(self.myfile, "a") as fp:
fp.write(salt.utils.stringutils.to_str("Goodbye" + os.linesep))
self.minion_run(
"file.line", self.myfile, "Hello", mode="ensure", before="Goodbye"
)
with salt.utils.files.fopen(self.myfile, "r") as fp:
content = fp.read()
self.assertEqual(content, "Hello" + os.linesep + "Goodbye" + os.linesep)
def test_file_read_bytes(self):
"""
Test that ``file.read`` reads and returns ``bytes`` data
"""
# Write some random bytes
data = b"n\x1a\xf7S@tBI\xa9J"
with salt.utils.files.fopen(self.myfile, "wb") as fp:
fp.write(data)
ret = self.minion_run("file.read", self.myfile, binary=True)
self.assertEqual(type(ret), bytes)
self.assertEqual(ret, data)
def test_file_read_str(self):
"""
Test that ``file.read`` reads and returns ``str`` data
"""
# Write some str data
data = "printable characters"
with salt.utils.files.fopen(self.myfile, "w") as fp:
fp.write(data)
ret = self.minion_run("file.read", self.myfile)
self.assertEqual(type(ret), str)
self.assertEqual(ret, data)