From 4326f0e62085f7ca6007f5e0ad6619b39ffba623 Mon Sep 17 00:00:00 2001 From: Artyom Lukianov Date: Mon, 20 Jun 2016 14:05:53 +0300 Subject: [PATCH] Copy file with correct permissions and owner (#58) * Copy file with correct permissions and owner * Patch Set #2 - move function to operatingsystem module to be consistent with os.stat - add function get_file_stats - add positive and negative tests * Patch Set #3 - fix tests * fix * Patch Set #4 - change function names - get octal permissions from stat * Patch Set #5 - fix tests - decode all output to UTF-8 * Patch Set #6 - remove decode of output * Patch Set #7 - fix functions names under copy_to function * Patch Set #8 - give possbility to change permission and ownership of the file, when file copied from other resource I believe it no need to check if ownership is a tuple, because we explicitly define it under docstring * Patch Set #9 - fix call to function fs.chown --- rrmngmnt/filesystem.py | 6 +- rrmngmnt/host.py | 27 ++++--- rrmngmnt/operatingsystem.py | 124 +++++++++++++++++++++++++---- tests/test_os.py | 151 ++++++++++++++++++++++++++++++++++++ 4 files changed, 284 insertions(+), 24 deletions(-) diff --git a/rrmngmnt/filesystem.py b/rrmngmnt/filesystem.py index 278887e..310dd20 100644 --- a/rrmngmnt/filesystem.py +++ b/rrmngmnt/filesystem.py @@ -1,6 +1,7 @@ import os -from rrmngmnt.service import Service + from rrmngmnt import errors +from rrmngmnt.service import Service class FileSystem(Service): @@ -10,11 +11,12 @@ class FileSystem(Service): """ def _exec_command(self, cmd): host_executor = self.host.executor() - rc, _, err = host_executor.run_cmd(cmd) + rc, out, err = host_executor.run_cmd(cmd) if rc: raise errors.CommandExecutionFailure( cmd=cmd, executor=host_executor, rc=rc, err=err ) + return out def _exec_file_test(self, op, path): return self.host.executor().run_cmd( diff --git a/rrmngmnt/host.py b/rrmngmnt/host.py index ac7feb0..c453e1d 100644 --- a/rrmngmnt/host.py +++ b/rrmngmnt/host.py @@ -3,23 +3,24 @@ It should hold methods / properties which returns you Instance of specific Service hosted on that Host. """ -import os import copy +import os import socket -import netaddr import warnings -from rrmngmnt import ssh +import netaddr + from rrmngmnt import errors from rrmngmnt import power_manager +from rrmngmnt import ssh from rrmngmnt.common import fqdn2ip -from rrmngmnt.network import Network -from rrmngmnt.storage import NFSService, LVMService -from rrmngmnt.service import Systemd, SysVinit, InitCtl -from rrmngmnt.resource import Resource from rrmngmnt.filesystem import FileSystem -from rrmngmnt.package_manager import PackageManagerProxy +from rrmngmnt.network import Network from rrmngmnt.operatingsystem import OperatingSystem +from rrmngmnt.package_manager import PackageManagerProxy +from rrmngmnt.resource import Resource +from rrmngmnt.service import Systemd, SysVinit, InitCtl +from rrmngmnt.storage import NFSService, LVMService class Host(Resource): @@ -243,7 +244,7 @@ def run_command( ) return rc, out, err - def copy_to(self, resource, src, dst): + def copy_to(self, resource, src, dst, mode=None, ownership=None): """ Copy to host from another resource @@ -253,12 +254,20 @@ def copy_to(self, resource, src, dst): :type src: str :param dst: path to destination :type dst: str + :param mode: file permissions + :type mode: str + :param ownership: file ownership(ex. ('root', 'root')) + :type ownership: tuple """ with resource.executor().session() as resource_session: with self.executor().session() as host_session: with resource_session.open_file(src, 'rb') as resource_file: with host_session.open_file(dst, 'wb') as host_file: host_file.write(resource_file.read()) + if mode: + self.fs.chmod(path=dst, mode=mode) + if ownership: + self.fs.chown(dst, *ownership) def _create_service(self, name, timeout): for provider in self.default_service_providers: diff --git a/rrmngmnt/operatingsystem.py b/rrmngmnt/operatingsystem.py index 517e4b2..a145410 100644 --- a/rrmngmnt/operatingsystem.py +++ b/rrmngmnt/operatingsystem.py @@ -14,15 +14,22 @@ def __init__(self, host): self._release_info = None self._dist = None - def get_release_str(self): - cmd = ['cat', '/etc/system-release'] - executor = self.host.executor() - rc, out, err = executor.run_cmd(cmd) + def _exec_command(self, cmd, err_msg=None): + host_executor = self.host.executor() + rc, out, err = host_executor.run_cmd(cmd) + if err_msg: + err = "{err_msg}: {err}".format(err_msg=err_msg, err=err) if rc: raise errors.CommandExecutionFailure( - executor, cmd, rc, - "Failed to obtain release string: {0}".format(err) + executor=host_executor, cmd=cmd, rc=rc, err=err ) + return out + + def get_release_str(self): + cmd = ['cat', '/etc/system-release'] + out = self._exec_command( + cmd=cmd, err_msg="Failed to obtain release string" + ) return out.strip() @property @@ -92,13 +99,9 @@ def get_distribution(self): "python", "-c", "import platform;print(','.join(platform.linux_distribution()))" ] - executor = self.host.executor() - rc, out, err = executor.run_cmd(cmd) - if rc: - raise errors.CommandExecutionFailure( - executor, cmd, rc, - "Failed to obtain release info: {0}".format(err) - ) + out = self._exec_command( + cmd=cmd, err_msg="Failed to obtain release info" + ) Distribution = namedtuple('Distribution', values) return Distribution(*[i.strip() for i in out.split(",")]) @@ -107,3 +110,98 @@ def distribution(self): if not self._dist: self._dist = self.get_distribution() return self._dist + + def stat(self, path): + """ + Get file or directory stats + + :return: file stats + :rtype: collections.namedtuple + """ + type_map = { + 'st_mode': ('0x%f', lambda x: int(x, 16)), + 'st_ino': ('%i', int), + 'st_dev': ('%d', int), + 'st_nlink': ('%h', int), + 'st_uid': ('%u', int), + 'st_gid': ('%g', int), + 'st_size': ('%s', int), + 'st_atime': ('%X', int), + 'st_mtime': ('%Y', int), + 'st_ctime': ('%W', int), + 'st_blocks': ('%b', int), + 'st_blksize': ('%o', int), + 'st_rdev': ('%t', int), + } + posix_stat_result = namedtuple( + "posix_stat_result", type_map.keys() + ) + + cmd = [ + "stat", + "-c", + ",".join(["%s=%s" % (k, v[0]) for k, v in type_map.items()]), + path + ] + out = self._exec_command(cmd=cmd) + out = out.strip().split(',') + + data = {} + + for pair in out: + key, value = pair.split('=') + data[key] = type_map[key][1](value) + + return posix_stat_result(**data) + + def get_file_permissions(self, path): + """ + Get file permissions + + :return: file permission in octal form(example 0644) + :rtype: str + """ + cmd = ["stat", "-c", "%a", path] + return self._exec_command(cmd=cmd).strip() + + def get_file_owner(self, path): + """ + Get file user and group owner name + + :return: file user and group owner names(example ['root', 'root']) + :rtype: list + """ + cmd = ["stat", "-c", "%U %G", path] + return self._exec_command(cmd=cmd).split() + + def user_exists(self, user_name): + """ + Check if user exist on system + + :param user_name: user name + :type user_name: str + :return: True, if user exist, otherwise False + :rtype: bool + """ + try: + cmd = ["id", "-u", user_name] + self._exec_command(cmd=cmd) + except errors.CommandExecutionFailure: + return False + return True + + def group_exists(self, group_name): + """" + Check if group exist on system + + :param group_name: group name + :type group_name: str + :return: True, if group exist, otherwise False + :rtype: bool + """ + try: + cmd = ["id", "-g", group_name] + self._exec_command(cmd=cmd) + except errors.CommandExecutionFailure: + return False + return True diff --git a/tests/test_os.py b/tests/test_os.py index 9622e74..1377029 100644 --- a/tests/test_os.py +++ b/tests/test_os.py @@ -175,3 +175,154 @@ def test_get_release_info(self): info = self.get_host().os.release_info assert 'VERSION_ID' not in info assert len(info) == 4 + + +type_map = { + 'st_mode': ('0x%f', lambda x: int(x, 16)), + 'st_ino': ('%i', int), + 'st_dev': ('%d', int), + 'st_nlink': ('%h', int), + 'st_uid': ('%u', int), + 'st_gid': ('%g', int), + 'st_size': ('%s', int), + 'st_atime': ('%X', int), + 'st_mtime': ('%Y', int), + 'st_ctime': ('%W', int), + 'st_blocks': ('%b', int), + 'st_blksize': ('%o', int), + 'st_rdev': ('%t', int), +} + + +class TestFileStats(object): + data = { + 'stat -c %s /tmp/test' % + ','.join(["%s=%s" % (k, v[0]) for k, v in type_map.items()]): ( + 0, + ( + 'st_ctime=0,' + 'st_rdev=0,' + 'st_blocks=1480,' + 'st_nlink=1,' + 'st_gid=0,' + 'st_dev=2051,' + 'st_ino=11804680,' + 'st_mode=0x81a4,' + 'st_mtime=1463487739,' + 'st_blksize=4096,' + 'st_size=751764,' + 'st_uid=0,' + 'st_atime=1463487196' + ), + '' + ), + 'stat -c "%U %G" /tmp/test': ( + 0, + 'root root', + '' + ), + 'stat -c %a /tmp/test': ( + 0, + '644\n', + '' + ), + 'id -u root': ( + 0, + '', + '' + ), + 'id -g root': ( + 0, + '', + '' + ) + } + files = {} + + @classmethod + def setup_class(cls): + fake_cmd_data(cls.data, cls.files) + + def get_host(self, ip='1.1.1.1'): + return Host(ip) + + def test_get_file_stats(self): + file_stats = self.get_host().os.stat('/tmp/test') + assert ( + file_stats.st_mode == 33188 and + file_stats.st_uid == 0 and + file_stats.st_gid == 0 + ) + + def test_get_file_owner(self): + file_user, file_group = self.get_host().os.get_file_owner('/tmp/test') + assert file_user == 'root' and file_group == 'root' + + def test_get_file_permissions(self): + assert self.get_host().os.get_file_permissions('/tmp/test') == '644' + + def test_user_exists(self): + assert self.get_host().os.user_exists('root') + + def test_group_exists(self): + assert self.get_host().os.group_exists('root') + + +class TestFileStatsNegative(object): + data = { + 'stat -c %s /tmp/negative_test' % + ','.join(["%s=%s" % (k, v[0]) for k, v in type_map.items()]): ( + 1, + '', + 'cannot stat ‘/tmp/negative_test’: No such file or directory' + ), + 'stat -c "%U %G" /tmp/negative_test': ( + 1, + '', + 'cannot stat ‘/tmp/negative_test’: No such file or directory' + ), + 'stat -c %a /tmp/negative_test': ( + 1, + '', + 'cannot stat ‘/tmp/negative_test’: No such file or directory' + ), + 'id -u test': ( + 1, + '', + '' + ), + 'id -g test': ( + 1, + '', + '' + ) + } + files = {} + + @classmethod + def setup_class(cls): + fake_cmd_data(cls.data, cls.files) + + def get_host(self, ip='1.1.1.1'): + return Host(ip) + + def test_get_file_stats(self): + with pytest.raises(errors.CommandExecutionFailure) as ex_info: + self.get_host().os.stat('/tmp/negative_test') + assert "No such file" in str(ex_info.value) + + def test_get_file_owner(self): + with pytest.raises(errors.CommandExecutionFailure) as ex_info: + self.get_host().os.get_file_owner('/tmp/negative_test') + assert "No such file" in str(ex_info.value) + + def test_get_file_permissions(self): + with pytest.raises(errors.CommandExecutionFailure) as ex_info: + self.get_host().os.get_file_permissions('/tmp/negative_test') + assert "No such file" in str(ex_info.value) + + def test_user_exists(self): + assert not self.get_host().os.user_exists('test') + + def test_group_exists(self): + assert not self.get_host().os.group_exists('test')