rootwrap模塊解析以及功能擴展

感謝朋友支持本博客,歡迎共同探討交流,由於能力和時間有限,錯誤之處在所難免,歡迎指正!

如果轉載,請保留作者信息。
博客地址:http://blog.csdn.net/gaoxingnengjisuan
郵箱地址:[email protected]

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目錄下的過濾器文件中,按照規則添加自己的命令行定義描述即可;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章