rootwrap模块解析以及功能扩展

感谢朋友支持本博客,欢迎共同探讨交流,由于能力和时间有限,错误之处在所难免,欢迎指正!

如果转载,请保留作者信息。
博客地址:http://blog.csdn.net/gaoxingnengjisuan
邮箱地址:dong.liu@siat.ac.cn

PS:因为各方面的原因好久没有写博客了,有时间还是要写一写的!


    使用rootwrap的目的就是针对系统某些特定的操作,让非特权用户以root用户的身份来安全地执行这些操作。据说nova曾经使用sudoers文件来列出允许执行的特权命令,使用sudo来运行这些命令,但是这样做不容易维护,而且不能进行复杂的参数处理(引自:http://blog.lightcloud.cn/?p=240)。早期版本我没有读过,而rootwrap的出现就是为了解决上述问题。


示例:

    对于文件要求root权限的文件/etc/iscsi/initiatorname.iscsi,如果我们在openstack系统中以非特权用户的身份来查看:

cat /etc/iscsi/initiatorname.iscsi
则会提示权限不足:Permission denied;而如果我们应用rootwrap模块对其进行命令行的封装:
sudo nova-rootwrap /etc/nova/rootwrap.conf cat /etc/iscsi/initiatorname.iscsi
就可以以非特权用户的身份在免输入密码的情况下顺利执行这条命令,得到想要的结果

InitiatorName=iqn.1994-05.com.redhat:5536dfb81ec0

    在这篇博客中,我们就以命令行

sudo nova-rootwrap /etc/nova/rootwrap.conf cat /etc/iscsi/initiatorname.iscsi
为例,来解析rootwrap模块具体的执行过程,以及功能扩展的方式。

注:至于配置文件/etc/nova/rootwrap.conf等的作用会在下面模块分析的过程中进行解析;


1.rootwrap模块解析

    rootwrap已经迁移到项目oslo中。我们以rootwrap在nova中的应用为例,在文件setup.cfg中可以看到nova-rootwrap的entrance为nova-rootwrap = oslo.rootwrap.cmd:main;

    首先来看方法:/oslo/rootwrap/cmd.py----def main:

def main():
    # Split arguments, require at least a command
    """
    sudo nova-rootwrap /etc/nova/rootwrap.conf cat /etc/iscsi/initiatorname.iscsi
    sys.argv = [
        '/usr/bin/nova-rootwrap', 
        '/etc/nova/rootwrap.conf', 
        'cat', 
        '/etc/iscsi/initiatorname.iscsi']
    """
    execname = sys.argv.pop(0)
    if len(sys.argv) < 2:
        _exit_error(execname, "No command specified", RC_NOCOMMAND, log=False)

    configfile = sys.argv.pop(0)
    userargs = sys.argv[:]
    """
    execname = /usr/bin/nova-rootwrap
    configfile = /etc/nova/rootwrap.conf
    userargs = ['cat', '/etc/iscsi/initiatorname.iscsi']
    """

    # Add ../ to sys.path to allow running from branch
    possible_topdir = os.path.normpath(os.path.join(os.path.abspath(execname),
                                                    os.pardir, os.pardir))
    """
    possible_topdir = /usr
    """
    
    if os.path.exists(os.path.join(possible_topdir, "oslo", "__init__.py")):
        sys.path.insert(0, possible_topdir)

    from oslo.rootwrap import wrapper

    # Load configuration
    try:
        rawconfig = moves.configparser.RawConfigParser()
        rawconfig.read(configfile)
        config = wrapper.RootwrapConfig(rawconfig)
    except ValueError as exc:
        msg = "Incorrect value in %s: %s" % (configfile, exc.message)
        _exit_error(execname, msg, RC_BADCONFIG, log=False)
    except moves.configparser.Error:
        _exit_error(execname, "Incorrect configuration file: %s" % configfile,
                    RC_BADCONFIG, log=False)

    if config.use_syslog:
        wrapper.setup_syslog(execname,
                             config.syslog_log_facility,
                             config.syslog_log_level)

    # Execute command if it matches any of the loaded filters
    filters = wrapper.load_filters(config.filters_path)
    """
    config.filters_path = ['/etc/nova/rootwrap.d', '/usr/share/nova/rootwrap']
    filters = 
        [
         <oslo.rootwrap.filters.CommandFilter object at 0x7f10f266ecd0>, 
         ......
         <oslo.rootwrap.filters.KillFilter object at 0x7f10f266efd0>, 
         ......
         <oslo.rootwrap.filters.EnvFilter object at 0x1744210>, 
         ......
         <oslo.rootwrap.filters.ReadFileFilter object at 0x1744ed0>, 
         ......
         <oslo.rootwrap.filters.RegExpFilter object at 0x174a1d0>, 
         ......
        ]
    注:遍历并加载/usr/share/nova/rootwrap路径下所有过滤文件中的命令行过滤对象;
    """
    
    try:
        filtermatch = wrapper.match_filter(filters, userargs,
                                           exec_dirs=config.exec_dirs)
        """
        userargs = ['cat', '/etc/iscsi/initiatorname.iscsi']
        exec_dirs = ['/sbin', '/usr/sbin', '/bin', '/usr/bin']
        注:exec_dirs为读取配置文件configfile = /etc/nova/rootwrap.conf获取;
        filtermatch = <oslo.rootwrap.filters.ReadFileFilter object at 0x2195e90>
        """
        
        if filtermatch:
            command = filtermatch.get_command(userargs,
                                              exec_dirs=config.exec_dirs)
            """
            userargs = ['cat', '/etc/iscsi/initiatorname.iscsi']
            exec_dirs = ['/sbin', '/usr/sbin', '/bin', '/usr/bin']
            command = ['sudo', '-u', 'shinian' ,'/bin/cat', '/etc/iscsi/initiatorname.iscsi']
            """
            
            if config.use_syslog:
                logging.info("(%s > %s) Executing %s (filter match = %s)" % (
                    _getlogin(), pwd.getpwuid(os.getuid())[0],
                    command, filtermatch.name))

            obj = subprocess.Popen(command,
                                   stdin=sys.stdin,
                                   stdout=sys.stdout,
                                   stderr=sys.stderr,
                                   preexec_fn=_subprocess_setup,
                                   env=filtermatch.get_environment(userargs))
            obj.wait()
            sys.exit(obj.returncode)

    except wrapper.FilterMatchNotExecutable as exc:
        msg = ("Executable not found: %s (filter match = %s)"
               % (exc.match.exec_path, exc.match.name))
        _exit_error(execname, msg, RC_NOEXECFOUND, log=config.use_syslog)

    except wrapper.NoFilterMatched:
        msg = ("Unauthorized command: %s (no filter matched)"
               % ' '.join(userargs))
        _exit_error(execname, msg, RC_UNAUTHORIZED, log=config.use_syslog)

    总体来说方法完成了六个步骤的工作:


1.1 命令行解析

    """
    sudo nova-rootwrap /etc/nova/rootwrap.conf cat /etc/iscsi/initiatorname.iscsi
    sys.argv = [
        '/usr/bin/nova-rootwrap', 
        '/etc/nova/rootwrap.conf', 
        'cat', 
        '/etc/iscsi/initiatorname.iscsi']
    """
    execname = sys.argv.pop(0)
    configfile = sys.argv.pop(0)
    userargs = sys.argv[:]
    """
    execname = /usr/bin/nova-rootwrap
    configfile = /etc/nova/rootwrap.conf
    userargs = ['cat', '/etc/iscsi/initiatorname.iscsi']
    """
    可以看到execname = /usr/bin/nova-rootwrap指定了一个脚本,查看其内容后,发现其所实现的功能是退出当前所运行的main方法,从后面所应用execname的代码也可以看出,其都应用在程序异常退出的场景中。

/usr/bin/nova-rootwrap脚本内容:

#!/usr/bin/python
# PBR Generated from u'console_scripts'

import sys
from oslo.rootwrap.cmd import main

if __name__ == "__main__":
    sys.exit(main())
再来看配置文件configfile = /etc/nova/rootwrap.conf,来看其内容:

# Configuration for nova-rootwrap
# This file should be owned by (and only-writeable by) the root user

[DEFAULT]
# List of directories to load filter definitions from (separated by ',').
# These directories MUST all be only writeable by root !
filters_path=/etc/nova/rootwrap.d,/usr/share/nova/rootwrap

# List of directories to search executables in, in case filters do not
# explicitely specify a full path (separated by ',')
# If not specified, defaults to system PATH environment variable.
# These directories MUST all be only writeable by root !
exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin

# Enable logging to syslog
# Default value is False
use_syslog=False

# Which syslog facility to use.
# Valid values include auth, authpriv, syslog, user0, user1...
# Default value is 'syslog'
syslog_log_facility=syslog

# Which messages to log.
# INFO means log all usage
# ERROR means only log unsuccessful attempts
syslog_log_level=ERROR
在这个配置文件中,主要指定了两方面的内容:

A.filters_path=/etc/nova/rootwrap.d,/usr/share/nova/rootwrap

    指定了若干过滤器文件所在的目录,这些过滤器文件有:api-metadata.filters  baremetal-compute-ipmi.filters  baremetal-deploy-helper.filters  compute.filters  network.filters;以compute.filters为例来看看其内容:

# nova-rootwrap command filters for compute nodes
# This file should be owned by (and only-writeable by) the root user

[Filters]
# nova/virt/disk/mount/api.py: 'kpartx', '-a', device
# nova/virt/disk/mount/api.py: 'kpartx', '-d', device
kpartx: CommandFilter, kpartx, root

# nova/virt/xenapi/vm_utils.py: tune2fs, -O ^has_journal, part_path
# nova/virt/xenapi/vm_utils.py: tune2fs, -j, partition_path
tune2fs: CommandFilter, tune2fs, root

# nova/virt/disk/mount/api.py: 'mount', mapped_device
# nova/virt/disk/api.py: 'mount', '-o', 'bind', src, target
# nova/virt/xenapi/vm_utils.py: 'mount', '-t', 'ext2,ext3,ext4,reiserfs'..
# nova/virt/configdrive.py: 'mount', device, mountdir
# nova/virt/libvirt/volume.py: 'mount', '-t', 'sofs' ...
mount: CommandFilter, mount, root

# nova/virt/disk/mount/api.py: 'umount', mapped_device
# nova/virt/disk/api.py: 'umount' target
# nova/virt/xenapi/vm_utils.py: 'umount', dev_path
# nova/virt/configdrive.py: 'umount', mountdir
umount: CommandFilter, umount, root

# nova/virt/libvirt/utils.py: 'blockdev', '--getsize64', path
# nova/virt/disk/mount/nbd.py: 'blockdev', '--flushbufs', device
blockdev: RegExpFilter, blockdev, root, blockdev, (--getsize64|--flushbufs), /dev/.*

# nova/virt/disk/vfs/localfs.py: 'tee', canonpath
tee: CommandFilter, tee, root

# nova/virt/xenapi/vm_utils.py: resize2fs, partition_path
# nova/virt/disk/api.py: resize2fs, image
resize2fs: CommandFilter, resize2fs, root

# nova/network/linux_net.py: 'kill', '-9', pid
# nova/network/linux_net.py: 'kill', '-HUP', pid
kill_dnsmasq: KillFilter, root, /usr/sbin/dnsmasq, -9, -HUP

# nova/network/linux_net.py: 'kill', pid
kill_radvd: KillFilter, root, /usr/sbin/radvd

# nova/network/linux_net.py: dnsmasq call
dnsmasq: EnvFilter, env, root, CONFIG_FILE=, NETWORK_ID=, dnsmasq

# nova/virt/libvirt/connection.py:
read_initiator: ReadFileFilter, /etc/iscsi/initiatorname.iscsi

# nova/utils.py:read_file_as_root: 'cat', file_path
# (called from nova/virt/disk/vfs/localfs.py:VFSLocalFS.read_file)
read_passwd: RegExpFilter, cat, root, cat, (/var|/usr)?/tmp/openstack-vfs-localfs[^/]+/etc/passwd
read_shadow: RegExpFilter, cat, root, cat, (/var|/usr)?/tmp/openstack-vfs-localfs[^/]+/etc/shadow
......

    可见在这些过滤器文件中,定义了很多具体命令的相关信息,如命令的应用场景,命令封装所调用的过滤器,命令的执行权限和命令的执行参数等等;我们初步就可以想到,这些过滤器文件中定义描述的所有命令,都是能够以非特权用户的身份在免输入密码的情况下执行的,我们用rootwrap模块来封装的命令行,首先应该到这些文件中查询是否有相匹配的命令,具体实现后面接着解析。


1.2 读取解析配置文件

    try:
        rawconfig = moves.configparser.RawConfigParser()
        rawconfig.read(configfile)
        config = wrapper.RootwrapConfig(rawconfig)
    except ValueError as exc:
        msg = "Incorrect value in %s: %s" % (configfile, exc.message)
        _exit_error(execname, msg, RC_BADCONFIG, log=False)
    except moves.configparser.Error:
        _exit_error(execname, "Incorrect configuration file: %s" % configfile,
                    RC_BADCONFIG, log=False)

    这段代码很明白,就是对配置文件configfile = /etc/nova/rootwrap.conf进行读取解析;


1.3 加载过滤器文件中定义的所有命令行过滤对象

    filters = wrapper.load_filters(config.filters_path)
    """
    config.filters_path = ['/etc/nova/rootwrap.d', '/usr/share/nova/rootwrap']
    filters = 
        [
         <oslo.rootwrap.filters.CommandFilter object at 0x7f10f266ecd0>, 
         ......
         <oslo.rootwrap.filters.KillFilter object at 0x7f10f266efd0>, 
         ......
         <oslo.rootwrap.filters.EnvFilter object at 0x1744210>, 
         ......
         <oslo.rootwrap.filters.ReadFileFilter object at 0x1744ed0>, 
         ......
         <oslo.rootwrap.filters.RegExpFilter object at 0x174a1d0>, 
         ......
        ]
    注:遍历并加载/usr/share/nova/rootwrap路径下所有过滤文件中的命令行过滤对象;
    """
    这段代码的作用就是遍历并加载/usr/share/nova/rootwrap路径下所有过滤文件(api-metadata.filters  baremetal-compute-ipmi.filters  baremetal-deploy-helper.filters  compute.filters  network.filters;以compute.filters)中的命令行过滤对象(系统安装后/etc/nova/rootwrap.d是不存在的);

    在1.1中和这里我们都可以看到用于命令行封装过滤的过滤器有很多,实际上共有CommandFilter、RegExpFilter、PathFilter、KillFilter、ReadFileFilter、IpFilter、EnvFilter、ChainingFilter和IpNetnsExecFilter共9种;这些过滤器类都以CommandFilter为父类,它们的具体区别主要体现在其方法match上,所以这9种过滤器主要是根据不同命令应用不同的匹配方式而区分实现的。


1.4 匹配到具体的命令行过滤对象

        filtermatch = wrapper.match_filter(filters, userargs,
                                           exec_dirs=config.exec_dirs)
        """
        userargs = ['cat', '/etc/iscsi/initiatorname.iscsi']
        exec_dirs = ['/sbin', '/usr/sbin', '/bin', '/usr/bin']
        注:exec_dirs为读取配置文件configfile = /etc/nova/rootwrap.conf获取;
        filtermatch = <oslo.rootwrap.filters.ReadFileFilter object at 0x2195e90>
        """

    这里的功能实现就是通过具体的命令行信息,调用不同的过滤器类方法,从定义描述的所有的命令行过滤对象中进行过滤匹配操作,看能否找到匹配的结果,如果找到匹配结果,就说明这个命令行是可以通过rootwrap模块封装来实现以非特权身份执行的;否则就说明openstack系统中指定此命令行只能以root身份执行。当然我们是可以通过进行功能扩展来实现我们的需求的。


1.5 通过rootwrap的封装获取具体的命令行实现

            command = filtermatch.get_command(userargs,
                                              exec_dirs=config.exec_dirs)
            """
            userargs = ['cat', '/etc/iscsi/initiatorname.iscsi']
            exec_dirs = ['/sbin', '/usr/sbin', '/bin', '/usr/bin']
            command = ['sudo', '-u', 'shinian' ,'/bin/cat', '/etc/iscsi/initiatorname.iscsi']
            """

    这段代码所实现的功能就是获取具体的命令行实现,实际上最后的命令行实现不过就是应用了我们所熟悉的sudo命令赋予普通用户的root权限,最后的命令行实际上就是:

sudo -u XXXX /bin/cat /etc/iscsi/initiatorname.iscsi
    来看代码,这里调用了前面所述过滤器类中的get_command方法,有的过滤器类重写了get_command方法,有的过滤器类没有重写,直接调用父类CommandFilter中的get_command方法,但是区别并不大,不过就是通过对命令行参数的加加减减,整合成最后的命令行实现并返回;

    这里调用的是过滤器ReadFileFilter,它直接调用了其父类CommandFilter的get_command方法,我们来简单看一下:

    def get_command(self, userargs, exec_dirs=[]):
        """
        Returns command to execute (with sudo -u if run_as != root).
        """
        """
        userargs = ['cat', '/etc/iscsi/initiatorname.iscsi']
        exec_dirs = ['/sbin', '/usr/sbin', '/bin', '/usr/bin']
        """
        
        to_exec = self.get_exec(exec_dirs=exec_dirs) or self.exec_path
        """
        to_exec = /bin/cat
        """
        
        if (self.run_as != 'root'):
            # Used to run commands at lesser privileges
            return ['sudo', '-u', self.run_as, to_exec] + userargs[1:]
        return [to_exec] + userargs[1:]

    这个方法很好理解,这里不再赘述;


1.6 派生一个子进程来执行命令行

            obj = subprocess.Popen(command,
                                   stdin=sys.stdin,
                                   stdout=sys.stdout,
                                   stderr=sys.stderr,
                                   preexec_fn=_subprocess_setup,
                                   env=filtermatch.get_environment(userargs))
            obj.wait()
            sys.exit(obj.returncode)

    这段代码所实现的功能就是派生一个新的子进程来执行rootwrap封装后的命令行,并等待其运行结束,当获取到命令行执行的返回值后退出这个子进程。


所以rootwarp实现的具体步骤就是:

    1 命令行解析
    2 读取解析配置文件
    3 加载过滤器文件中定义的所有命令行过滤对象
    4 匹配到具体的命令行过滤对象
    5 通过rootwrap的封装获取具体的命令行实现
    6 派生一个子进程来执行命令行
    实际上就是把命令行:
    sudo nova-rootwrap /etc/nova/rootwrap.conf cat /etc/iscsi/initiatorname.iscsi
经过若干参数处理和匹配操作转化为:
    sudo -u XXXX /bin/cat /etc/iscsi/initiatorname.iscsi
之后,启动一个子进程来实现这个命令行的执行操作;


2.rootwrap模块的功能扩展

    如果我们需要针对特定的命令操作,需要实现以非特权身份来执行root用户才能执行的命令,在这个模块中就可以进行自己的功能扩展,当然前提是要充分理解不同的过滤器类的具体区别。功能扩展有两种:

    1.直接在/usr/share/nova/rootwrap目录下的过滤器文件中,按照规则添加自己的命令行定义描述即可;

    2.如果当前的过滤器类实现无法满足命令行匹配的要求,则需要在/oslo/rootwrap/fifter.py中定义新的过滤器类,继承自类CommandFilter,并根据具体需求重写match和get_command等方法;然后再在/usr/share/nova/rootwrap目录下的过滤器文件中,按照规则添加自己的命令行定义描述即可;

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值