RootWarp

  • sudoer文件的作用
openstack在实际过程中会调用很多外部命令,例如就network服务而言就有:`ip`,`ovs-vsctl`,`iptables`,`dnsmasq`,`brctl`等等,这些命令在linux中都是需要以root身份来运行的,如果是普通用户通常的做法是在命令前加`sudo`切换到root身份再执行这些命令,但是这个方法在执行命令的时候需要输入密码确认操作,为了避免输入密码,我们需要配置一下sudo.

建议的方法是在/etc/sudoers.d/目录下新建一个文件,例如:

[root@localhost sudoers.d]# pwd
/etc/sudoers.d
[root@localhost sudoers.d]# echo 'zhengtianbao ALL = (root) NOPASSWD: ALL' > stack_sh
[root@localhost sudoers.d]# cat stack_sh
zhengtianbao ALL = (root) NOPASSWD: ALL
这样 当我们切换到’zhengtianbao’这个用户的时候,只要在想执行的命令前加’sudo’,不需要输入密码就能以root身份运行.
[root@localhost sudoers.d]# su zhengtianbao
[zhengtianbao@localhost sudoers.d]$ ls
ls: cannot open directory .: Permission denied
[zhengtianbao@localhost sudoers.d]$ sudo ls
stack_sh

关于sudoers的配置文件如何定义,这里简单介绍下:

通用格式:
user host run_as command
user:一位或几位用户,在/etc/group中可以用一个%代替它,组对象的名称一定要用百分号%开头.
host:一个或几个主机名.
run_as:作为哪个用户运行,常见选项是root和ALL.
command:想让用户或组运行的一个或几个根级别命令.
例如:
hans ALL=(root) useradd,userdel
授权hans用户在所有计算机上以root身份运行useradd,userdel命令.
%smith ALL=(ALL) NOPASSWD:useradd,userdel
授权smith组全部成员在所有计算机上以所有用户的身份运行useradd,userdel命令;且运行时不必输入密码.
一点点疑问:能否控制命令的参数呢?接下来做个测试:

[root@localhost sudoers.d]# echo 'zhengtianbao ALL = (root) NOPASSWD: /bin/ls -l, /bin/ls -a' > stack_sh
[root@localhost sudoers.d]# su zhengtianbao
[zhengtianbao@localhost sudoers.d]$ ls -a
ls: cannot open directory .: Permission denied
[zhengtianbao@localhost sudoers.d]$ sudo ls -a
.  ..  stack_sh
[zhengtianbao@localhost sudoers.d]$ sudo ls -l
total 8
-rw-r--r-- 1 root root  59 Jan 26 14:43 stack_sh
[zhengtianbao@localhost sudoers.d]$ sudo ls -a -l
[sudo] password for zhengtianbao: 

可见能够控制的命令参数还是很严格的.
放在openstack中这也是可行的,但是随着项目的增大,单纯的修改sudoers影响了openstack的可维护性,因此引入了root warpper来管理命令权限相关的内容.

  • devstack使用普通用户安装openstack,无需密码执行所有root命令

    如果是根据devstack来安装openstack的话,查看devstack中的stack.sh里面有关于root权限的内容:

    # root Access
    # -----------
     
    # OpenStack is designed to be run as a non-root user; Horizon will fail to run
    # as **root** since Apache will not serve content from **root** user).
    # ``stack.sh`` must not be run as **root**.  It aborts and suggests one course of
    # action to create a suitable user account.
     
    if [[ $EUID -eq 0 ]]; then
        echo "You are running this script as root."
        echo "Cut it out."
        echo "Really."
        echo "If you need an account to run DevStack, do this (as root, heh) to create $STACK_USER:"
        echo "$TOP_DIR/tools/create-stack-user.sh"
        exit 1
    fi
     
    # We're not **root**, make sure ``sudo`` is available
    is_package_installed sudo || install_package sudo

    好,它这里表示需要先通过脚本tools/create-stack-user.sh来创建一个用户,再通过那个用户来执行,看脚本内容:

    # Needed to get ``ENABLED_SERVICES``
    source $TOP_DIR/stackrc
     
    # Give the non-root user the ability to run as **root** via ``sudo``
    is_package_installedsudo || install_package sudo
         
    if ! getent group $STACK_USER >/dev/null;then
        echo "Creating a group called $STACK_USER"
        groupadd $STACK_USER 
    fi 
         
    if ! getent passwd $STACK_USER >/dev/null;then
        echo "Creating a user called $STACK_USER"
        useradd -g $STACK_USER -s /bin/bash -d $DEST -m $STACK_USER
    fi
     
    echo "Giving stack user passwordless sudo privileges"
    # UEC images ``/etc/sudoers`` does not have a ``#includedir``, add one
    grep -q"^#includedir.*/etc/sudoers.d" /etc/sudoers ||
        echo "#includedir /etc/sudoers.d" >>/etc/sudoers
    (umask 226 && echo "$STACK_USER ALL=(ALL) NOPASSWD:ALL" \
        >/etc/sudoers.d/50_stack_sh )

    $STACK_USER的值在stackrc文件中定义,当前环境是root身份时则为’stack’:

    # Determine stack user
    if [[ $EUID -eq 0 ]]; then
        STACK_USER=stack
    else
        STACK_USER=$(whoami)
    fi

    ok,这里发现它在/etc/sudoers.d/目录下生成了一个50_stack_sh的文件,里面的内容是:

    stack ALL=(ALL) NOPASSWD:ALL
    显然它创建的stack用户现在可以在使用`sudo`执行任何命令都能省略输入密码的过程了.

    • devstack安装nova,配置以nova身份运行命令`sudo /usr/bin/nova-rootwrap /etc/nova/rootwrap.conf * 不需要输入密码

      接下来继续看nova的安装过程,在devstack/lib/目录下的nova脚本中有configure_nova()的方法,它会在stack.sh中被调用到,正如名字所示,它用来设置nova的config文件,创建一些数据等工作:

      # configure_nova() - Set config files, create data dirs, etc
      function configure_nova() {
          # Put config files in ``/etc/nova`` for everyone to find
          if [[ ! -d $NOVA_CONF_DIR ]]; then
              sudo mkdir -p $NOVA_CONF_DIR
          fi
          sudo chown $STACK_USER $NOVA_CONF_DIR
       
          cp -p $NOVA_DIR/etc/nova/policy.json $NOVA_CONF_DIR
       
          configure_nova_rootwrap
          ...

      注意里面的configure_nova_rootwrap,查看该方法:

      # configure_nova_rootwrap() - configure Nova's rootwrap
      function configure_nova_rootwrap() {
          # Deploy new rootwrap filters files (owned by root).
          # Wipe any existing rootwrap.d files first
          if [[ -d $NOVA_CONF_DIR/rootwrap.d ]]; then
              sudo rm -rf $NOVA_CONF_DIR/rootwrap.d
          fi
          # Deploy filters to /etc/nova/rootwrap.d
          sudo mkdir -m 755 $NOVA_CONF_DIR/rootwrap.d
          sudo cp $NOVA_DIR/etc/nova/rootwrap.d/*.filters $NOVA_CONF_DIR/rootwrap.d
          sudo chown -R root:root $NOVA_CONF_DIR/rootwrap.d
          sudo chmod 644 $NOVA_CONF_DIR/rootwrap.d/*
          # Set up rootwrap.conf, pointing to /etc/nova/rootwrap.d
          sudo cp $NOVA_DIR/etc/nova/rootwrap.conf $NOVA_CONF_DIR/
          sudo sed -e"s:^filters_path=.*$:filters_path=$NOVA_CONF_DIR/rootwrap.d:" -i $NOVA_CONF_DIR/rootwrap.conf
          sudo chown root:root $NOVA_CONF_DIR/rootwrap.conf
          sudo chmod 0644 $NOVA_CONF_DIR/rootwrap.conf
          # Specify rootwrap.conf as first parameter to nova-rootwrap
          ROOTWRAP_SUDOER_CMD="$NOVA_ROOTWRAP $NOVA_CONF_DIR/rootwrap.conf *"
       
          # Set up the rootwrap sudoers for nova
          TEMPFILE=`mktemp`
          echo "$STACK_USER ALL=(root) NOPASSWD: $ROOTWRAP_SUDOER_CMD" >$TEMPFILE
          chmod 0440 $TEMPFILE
          sudo chown root:root $TEMPFILE
          sudo mv $TEMPFILE/etc/sudoers.d/nova-rootwrap
      }

      显然,它在/etc/sudoers.d/目录下创建了nova-rootwrap的文件,里面的内容可能是:
      nova ALL = (root) NOPASSWD: /usr/bin/nova-rootwrap /etc/nova/rootwrap.conf *
      nova:指用户名.
      ALL:指主机名.
      root:指运行用户名.
      NOPASSWD:指运行下面命令时不需要输入密码.
      /usr/bin/nova-rootwrap /etc/nova/rootwrap.conf *:指能够运行的命令.
      上面的文件定义就是说:
      以nova身份运行命令`sudo /usr/bin/nova-rootwrap /etc/nova/rootwrap.conf * `
      时是不需要输入密码的,其中的’*’指的是任意字符串,例如:`ip route show …`.

      • nova中执行外部命令怎么调用nova-rootwarp

      /usr/bin/nova-rootwrap是一个可执行的脚本文件,/etc/nova/rootwrap.conf则是rootwrap相关的配置,里面定义了filters-path所在路径,以及缺省的可执行命令所在路径,具体的过滤逻辑如下:
      1)获取要执行的命令,如:ip
      2)通过filters-path加载配置文件中定义的可以执行的命令列表
      3)判断命令是否在可执行命令列表中
      4)若在则通过python的subprocess模块执行Popen方法;不在则给出错误信息,退出.
      这些都可以在nova的bin/nova-rootwrap文件中查看.

      所有的nova代码在执行外部命令的时候都会用到execute函数,这个函数定义在nova顶层目录下的utils.py模块下.
      例如:
      from nova import utils
      utils.execute(‘chmod’, ’777′, tmpdir, run_as_root=True)
      execute函数首先根据run_as_root参数进行了一些处理,如下所示:

      def _get_root_helper():
          return 'sudo nova-rootwrap %s' % CONF.rootwrap_config
       
       
      def execute(*cmd,**kwargs):
          """Convenience wrapper around oslo's execute() method."""
          if 'run_as_root' in kwargsand not 'root_helper' in kwargs:
              kwargs['root_helper']= _get_root_helper()
          return processutils.execute(*cmd,**kwargs)

      然后丢给processutils中的execute,查看代码:

      def execute(*cmd,**kwargs):
          ...
          if run_as_rootand hasattr(os,'geteuid')and os.geteuid() != 0:
              if not root_helper:
                  raise NoRootWrapSpecified(
                      message=_('Command requested root, but did not '
                                'specify a root helper.'))
              cmd= shlex.split(root_helper)+ list(cmd)
       
          cmd= map(str, cmd)
       
          while attempts > 0:
              ...
              obj= subprocess.Popen(cmd,
                                     stdin=_PIPE,
                                     stdout=_PIPE,
                                     stderr=_PIPE,
                                     close_fds=close_fds,
                                     preexec_fn=preexec_fn,
                                     shell=shell)
              ...

      最终执行的命令cmd就是`sudo /usr/bin/nova-rootwrap /etc/nova/rootwrap.conf chmod 777 tmpdir`

      这里`chmod`这个命令在rootwrap.d目录下的filters文件中可以找到对应的配置:
      chmod: CommandFilter, chmod, root
      CommandFilter的定义是在oslo的rootwrap/filters.py中,里面还定义了其他的filter(RegExp,Path,Kill,ReadFile,Ip,Env,Chaining,IpNetnsExec).
      更加详细的内容请查看nova/rootwarp目录下的filters.py与wrapper.py

      • rootwarp模块详析

        rootwrap已经迁移到项目oslo中。

        我们以rootwrap在nova中的应用为例,在文件setup.cfg中可以看到nova-rootwrap的entrance为nova-rootwrap = oslo.rootwrap.cmd:main;也就是oslo_rootwrap/cmp.py的main函数,下面仔细介绍这个main函数的流程:


        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模块来封装的命令行,首先应该到这些文件中查询是否有相匹配的命令,具体实现后面接着解析。


         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进行读取解析;

         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是不存在的);

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


        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身份执行。当然我们是可以通过进行功能扩展来实现我们的需求的。


        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:] 

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


        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
        之后,启动一个子进程来实现这个命令行的执行操作;

        =======================如下为原创==========================

        7.各种Filter详解

        1)CommandFilter
        对应的处理类为:
        class CommandFilter(object):
            """Command filter only checking that the 1st argument matches exec_path."""
        
            def __init__(self, exec_path, run_as, *args):
                self.name = ''
                self.exec_path = exec_path
                self.run_as = run_as
                self.args = args
                self.real_exec = None
        
            def get_exec(self, exec_dirs=None):
                """Returns existing executable, or empty string if none found."""
                exec_dirs = exec_dirs or []
                if self.real_exec is not None:
                    return self.real_exec
                self.real_exec = ""
                if os.path.isabs(self.exec_path):
                    if os.access(self.exec_path, os.X_OK):
                        self.real_exec = self.exec_path
                else:
                    for binary_path in exec_dirs:
                        expanded_path = os.path.join(binary_path, self.exec_path)
                        if os.access(expanded_path, os.X_OK):
                            self.real_exec = expanded_path
                            break
                return self.real_exec
        
            def match(self, userargs):
                """Only check that the first argument (command) matches exec_path."""
                return userargs and os.path.basename(self.exec_path) == userargs[0]
        
            def preexec(self):
                """Setuid in subprocess right before command is invoked."""
                if self.run_as != 'root':
                    os.setuid(_getuid(self.run_as))
        
            def get_command(self, userargs, exec_dirs=None):
                """Returns command to execute."""
                exec_dirs = exec_dirs or []
                to_exec = self.get_exec(exec_dirs=exec_dirs) or self.exec_path
                return [to_exec] + userargs[1:]
        
            def get_environment(self, userargs):
                """Returns specific environment to set, None if none."""
                return None
        典型配置为:
        qemu-nbd: CommandFilter, qemu-nbd, root

        对应调用代码为:

        nova/blob/master/nova/virt/disk/mount/nbd.py
        _out, err = utils.trycmd('qemu-nbd', '-c', device,
                                         self.image.path,
                                         run_as_root=True)

        首先,程序开始读到配置,会初始化一个名称为qemu-nbd的CommandFilter实例,参数是exec_path=mount,run_as=root
        代码调用的时候,match函数的参数usrargs=['qemu-nbd', '-c', device],因此调用匹配实例,于是使用这个实例的get_command函数生成最终调用的命令:sudo -u /bin/qemu-nbd -c device
        2)RegExpFilter
        class RegExpFilter(CommandFilter):
            """Command filter doing regexp matching for every argument."""
        
            def match(self, userargs):
                # Early skip if command or number of args don't match
                if (not userargs or len(self.args) != len(userargs)):
                    # DENY: argument numbers don't match
                    return False
                # Compare each arg (anchoring pattern explicitly at end of string)
                for (pattern, arg) in zip(self.args, userargs):
                    try:
                        if not re.match(pattern + '$', arg):
                            # DENY: Some arguments did not match
                            return False
                    except re.error:
                        # DENY: Badly-formed filter
                        return False
                # ALLOW: All arguments matched
                return True
        其中zip函数:a=(1,2),b=(3,4), zip(a,b)=[(1, 3), (2, 4)]
        典型配置为:
        blockdev: RegExpFilter, blockdev, root, blockdev, (--getsize64|--flushbufs), /dev/.*
        对应调用代码为:
        nova/blob/master/nova/virt/disk/mount/nbd.py
        utils.execute('blockdev', '--flushbufs',
                                  self.device, run_as_root=True)
        3)PathFilter
        class PathFilter(CommandFilter):
            """Command filter checking that path arguments are within given dirs
        
                One can specify the following constraints for command arguments:
                    1) pass     - pass an argument as is to the resulting command
                    2) some_str - check if an argument is equal to the given string
                    3) abs path - check if a path argument is within the given base dir
        
                A typical rootwrapper filter entry looks like this:
                    # cmdname: filter name, raw command, user, arg_i_constraint [, ...]
                    chown: PathFilter, /bin/chown, root, nova, /var/lib/images
        
            """
        
            def match(self, userargs):
                if not userargs or len(userargs) < 2:
                    return False
        
                arguments = userargs[1:]
        
                equal_args_num = len(self.args) == len(arguments)
                exec_is_valid = super(PathFilter, self).match(userargs)
                args_equal_or_pass = all(
                    arg == 'pass' or arg == value
                    for arg, value in zip(self.args, arguments)
                    if not os.path.isabs(arg)  # arguments not specifying abs paths
                )
                paths_are_within_base_dirs = all(
                    os.path.commonprefix([arg, os.path.realpath(value)]) == arg
                    for arg, value in zip(self.args, arguments)
                    if os.path.isabs(arg)  # arguments specifying abs paths
                )
        
                return (equal_args_num and
                        exec_is_valid and
                        args_equal_or_pass and
                        paths_are_within_base_dirs)
        
            def get_command(self, userargs, exec_dirs=None):
                exec_dirs = exec_dirs or []
                command, arguments = userargs[0], userargs[1:]
        
                # convert path values to canonical ones; copy other args as is
                args = [os.path.realpath(value) if os.path.isabs(arg) else value
                        for arg, value in zip(self.args, arguments)]
        
                return super(PathFilter, self).get_command([command] + args,
                                                           exec_dirs)
        
        典型配置为:




        4)KillFilter
        代码为:
        class KillFilter(CommandFilter):
            """Specific filter for the kill calls.
               1st argument is the user to run /bin/kill under
               2nd argument is the location of the affected executable
                   if the argument is not absolute, it is checked against $PATH
               Subsequent arguments list the accepted signals (if any)
               This filter relies on /proc to accurately determine affected
               executable, so it will only work on procfs-capable systems (not OSX).
            """
        
            def __init__(self, *args):
                super(KillFilter, self).__init__("/bin/kill", *args)
        
            @staticmethod
            def _program_path(command):
                """Try to determine the full path for command.
                Return command if the full path cannot be found.
                """
        
                # shutil.which() was added to Python 3.3
                if hasattr(shutil, 'which'):
                    return shutil.which(command)
        
                if os.path.isabs(command):
                    return command
        
                path = os.environ.get('PATH', os.defpath).split(os.pathsep)
                for dir in path:
                    program = os.path.join(dir, command)
                    if os.path.isfile(program):
                        return program
        
                return command
        
            def _program(self, pid):
                """Determine the program associated with pid"""
        
                try:
                    command = os.readlink("/proc/%d/exe" % int(pid))
                except (ValueError, EnvironmentError):
                    # Incorrect PID
                    return None
        
                # NOTE(yufang521247): /proc/PID/exe may have '\0' on the
                # end (ex: if an executable is updated or deleted), because python
                # doesn't stop at '\0' when read the target path.
                command = command.partition('\0')[0]
        
                # NOTE(dprince): /proc/PID/exe may have ' (deleted)' on
                # the end if an executable is updated or deleted
                if command.endswith(" (deleted)"):
                    command = command[:-len(" (deleted)")]
        
                if os.path.isfile(command):
                    return command
        
                # /proc/PID/exe may have been renamed with
                # a ';......' or '.#prelink#......' suffix etc.
                # So defer to /proc/PID/cmdline in that case.
                try:
                    with open("/proc/%d/cmdline" % int(pid)) as pfile:
                        cmdline = pfile.read().partition('\0')[0]
        
                    cmdline = self._program_path(cmdline)
                    if os.path.isfile(cmdline):
                        command = cmdline
        
                    # Note we don't return None if cmdline doesn't exist
                    # as that will allow killing a process where the exe
                    # has been removed from the system rather than updated.
                    return command
                except EnvironmentError:
                    return None
        
            def match(self, userargs):
                if not userargs or userargs[0] != "kill":
                    return False
                args = list(userargs)
                if len(args) == 3:
                    # A specific signal is requested
                    signal = args.pop(1)
                    if signal not in self.args[1:]:
                        # Requested signal not in accepted list
                        return False
                else:
                    if len(args) != 2:
                        # Incorrect number of arguments
                        return False
                    if len(self.args) > 1:
                        # No signal requested, but filter requires specific signal
                        return False
        
                command = self._program(args[1])
                if not command:
                    return False
        
                kill_command = self.args[0]
        
                if os.path.isabs(kill_command):
                    return kill_command == command
        
                return (os.path.isabs(command) and
                        kill_command == os.path.basename(command) and
                        os.path.dirname(command) in os.environ.get('PATH', ''
                                                                   ).split(':'))
        典型配置为:
        kill_dnsmasq: KillFilter, root, /usr/sbin/dnsmasq, -9, -HUP
        对应调用为:
        _execute('kill', '-HUP', pid, run_as_root=True)
        5)ReadFileFilter
        代码为:
        class ReadFileFilter(CommandFilter):
            """Specific filter for the utils.read_file_as_root call."""
        
            def __init__(self, file_path, *args):
                self.file_path = file_path
                super(ReadFileFilter, self).__init__("/bin/cat", "root", *args)
        
            def match(self, userargs):
                return (userargs == ['cat', self.file_path])
        典型配置为:
        read_initiator: ReadFileFilter, /etc/iscsi/initiatorname.iscsi

        对应调用为:
        6)IpFilter
        代码为:
        class IpFilter(CommandFilter):
            """Specific filter for the ip utility to that does not match exec."""
        
            def match(self, userargs):
                if userargs[0] == 'ip':
                    # Avoid the 'netns exec' command here
                    for a, b in zip(userargs[1:], userargs[2:]):
                        if a == 'netns':
                            return (b != 'exec')
                    else:
                        return True
        典型配置为:


        7)
        代码为:
        class EnvFilter(CommandFilter):
            """Specific filter for the env utility.
            Behaves like CommandFilter, except that it handles
            leading env A=B.. strings appropriately.
            """
        
            def _extract_env(self, arglist):
                """Extract all leading NAME=VALUE arguments from arglist."""
        
                envs = set()
                for arg in arglist:
                    if '=' not in arg:
                        break
                    envs.add(arg.partition('=')[0])
                return envs
        
            def __init__(self, exec_path, run_as, *args):
                super(EnvFilter, self).__init__(exec_path, run_as, *args)
        
                env_list = self._extract_env(self.args)
                # Set exec_path to X when args are in the form of
                # env A=a B=b C=c X Y Z
                if "env" in exec_path and len(env_list) < len(self.args):
                    self.exec_path = self.args[len(env_list)]
        
            def match(self, userargs):
                # ignore leading 'env'
                if userargs[0] == 'env':
                    userargs.pop(0)
        
                # require one additional argument after configured ones
                if len(userargs) < len(self.args):
                    return False
        
                # extract all env args
                user_envs = self._extract_env(userargs)
                filter_envs = self._extract_env(self.args)
                user_command = userargs[len(user_envs):len(user_envs) + 1]
        
                # match first non-env argument with CommandFilter
                return (super(EnvFilter, self).match(user_command)
                        and len(filter_envs) and user_envs == filter_envs)
        
            def exec_args(self, userargs):
                args = userargs[:]
        
                # ignore leading 'env'
                if args[0] == 'env':
                    args.pop(0)
        
                # Throw away leading NAME=VALUE arguments
                while args and '=' in args[0]:
                    args.pop(0)
        
                return args
        
            def get_command(self, userargs, exec_dirs=[]):
                to_exec = self.get_exec(exec_dirs=exec_dirs) or self.exec_path
                return [to_exec] + self.exec_args(userargs)[1:]
        
            def get_environment(self, userargs):
                env = os.environ.copy()
        
                # ignore leading 'env'
                if userargs[0] == 'env':
                    userargs.pop(0)
        
                # Handle leading NAME=VALUE pairs
                for a in userargs:
                    env_name, equals, env_value = a.partition('=')
                    if not equals:
                        break
                    if env_name and env_value:
                        env[env_name] = env_value
        
                return env
        典型配置为:
        dnsmasq: EnvFilter, env, root, CONFIG_FILE=, NETWORK_ID=, dnsmasq
        调用代码为:
            cmd = ['env',
                   'CONFIG_FILE=%s' % jsonutils.dumps(CONF.dhcpbridge_flagfile),
                   'NETWORK_ID=%s' % str(network_ref['id']),
                   'dnsmasq',
                   '--strict-order',
                   '--bind-interfaces',
                   '--conf-file=%s' % CONF.dnsmasq_config_file,
                   '--pid-file=%s' % _dhcp_file(dev, 'pid'),
                   '--dhcp-optsfile=%s' % _dhcp_file(dev, 'opts'),
                   '--listen-address=%s' % network_ref['dhcp_server'],
                   '--except-interface=lo',
                   '--dhcp-range=set:%s,%s,static,%s,%ss' %
                                 (network_ref['label'],
                                  network_ref['dhcp_start'],
                                  network_ref['netmask'],
                                  CONF.dhcp_lease_time),
                   '--dhcp-lease-max=%s' % len(netaddr.IPNetwork(network_ref['cidr'])),
                   '--dhcp-hostsfile=%s' % _dhcp_file(dev, 'conf'),
                   '--dhcp-script=%s' % CONF.dhcpbridge,
                   '--no-hosts',
                   '--leasefile-ro']
        
            # dnsmasq currently gives an error for an empty domain,
            # rather than ignoring.  So only specify it if defined.
            if CONF.dhcp_domain:
                cmd.append('--domain=%s' % CONF.dhcp_domain)
        
            dns_servers = CONF.dns_server
            if CONF.use_network_dns_servers:
                if network_ref.get('dns1'):
                    dns_servers.append(network_ref.get('dns1'))
                if network_ref.get('dns2'):
                    dns_servers.append(network_ref.get('dns2'))
            if network_ref['multi_host']:
                cmd.append('--addn-hosts=%s' % _dhcp_file(dev, 'hosts'))
            if dns_servers:
                cmd.append('--no-resolv')
            for dns_server in dns_servers:
                cmd.append('--server=%s' % dns_server)
        
            _execute(*cmd, run_as_root=True)
        8)IpNetnsExecFilter
        class IpNetnsExecFilter(ChainingFilter):
            """Specific filter for the ip utility to that does match exec."""
        
            def match(self, userargs):
                # Network namespaces currently require root
                # require <ns> argument
                if self.run_as != "root" or len(userargs) < 4:
                    return False
        
                return (userargs[:3] == ['ip', 'netns', 'exec'])
        
            def exec_args(self, userargs):
                args = userargs[4:]
                if args:
                    args[0] = os.path.basename(args[0])
                return args
        
        典型配置为:

        调用代码为:


        9)Chain
        class ChainingFilter(CommandFilter):
            def exec_args(self, userargs):
                return []
        class ChainingRegExpFilter(ChainingFilter):
            """Command filter doing regexp matching for prefix commands.
            Remaining arguments are filtered again. This means that the command
            specified as the arguments must be also allowed to execute directly.
            """
        
            def match(self, userargs):
                # Early skip if number of args is smaller than the filter
                if (not userargs or len(self.args) > len(userargs)):
                    return False
                # Compare each arg (anchoring pattern explicitly at end of string)
                for (pattern, arg) in zip(self.args, userargs):
                    try:
                        if not re.match(pattern + '$', arg):
                            # DENY: Some arguments did not match
                            return False
                    except re.error:
                        # DENY: Badly-formed filter
                        return False
                # ALLOW: All arguments matched
                return True
        
            def exec_args(self, userargs):
                args = userargs[len(self.args):]
                if args:
                    args[0] = os.path.basename(args[0])
                return args
        评论
        添加红包

        请填写红包祝福语或标题

        红包个数最小为10个

        红包金额最低5元

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

        抵扣说明:

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

        余额充值