[ English | 日本語 | Deutsch | Indonesia ]

定制对象存储 (Swift) 中间件

OpenStack 对象存储,在代码中被称为 swift,基于 Python Paste 框架。 最佳的架构介绍可以在 Read The Docs 上找到。 由于 swift 项目使用此框架,因此您可以通过将一些自定义代码放置在项目的管道中来添加功能,而无需更改任何核心代码。

想象一下这样一个场景:您有一个容器具有公共访问权限,但实际上您只想将其访问权限限制为基于白名单的一组 IP。 在此示例中,我们将为 swift 创建一个中间件,该中间件仅允许从一组 IP 地址访问容器,这些 IP 地址由容器的元数据项确定。 只有您使用容器的元数据显式列入白名单的 IP 地址才能访问该容器。

警告

此示例仅用于说明目的。 在没有进一步开发和广泛的安全测试的情况下,不应将其用作容器 IP 白名单解决方案。

当您加入 stack.sh 启动的 screen 会话,使用 screen -r stack 时,您会看到每个正在运行的服务的一个 screen,具体数量取决于您配置 DevStack 运行的服务数量。

星号 * 表示您正在查看的 screen 窗口。 此示例显示我们正在查看 key (用于 keystone) screen 窗口

0$ shell  1$ key*  2$ horizon  3$ s-proxy  4$ s-object  5$ s-container  6$ s-account

screen 窗口的用途如下

shell

一个 shell,您可以在其中完成一些工作

key*

keystone 服务

horizon

horizon 仪表板 Web 应用程序

s-{name}

swift 服务

要创建中间件并通过 Paste 配置将其插入

OpenStack 的所有代码都位于 /opt/stack 中。 转到 shell screen 中的 swift 目录并编辑您的中间件模块。

  1. 更改到安装对象存储的目录

    $ cd /opt/stack/swift
    
  2. 创建 ip_whitelist.py Python 源代码文件

    $ vim swift/common/middleware/ip_whitelist.py
    
  3. 将以下代码复制到 ip_whitelist.py 中。 以下代码是一个中间件示例,它根据 IP 地址限制对容器的访问,如本节开头所述。 中间件将请求传递给另一个应用程序。 此示例使用 swift “swob” 库将 Web Server Gateway Interface (WSGI) 请求和响应包装到 swift 可以交互的对象中。 完成后,保存并关闭文件。

    # vim: tabstop=4 shiftwidth=4 softtabstop=4
    # Copyright (c) 2014 OpenStack Foundation
    # All Rights Reserved.
    #
    #    Licensed under the Apache License, Version 2.0 (the "License"); you may
    #    not use this file except in compliance with the License. You may obtain
    #    a copy of the License at
    #
    #         https://apache.org/licenses/LICENSE-2.0
    #
    #    Unless required by applicable law or agreed to in writing, software
    #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    #    License for the specific language governing permissions and limitations
    #    under the License.
    
    import socket
    
    from swift.common.utils import get_logger
    from swift.proxy.controllers.base import get_container_info
    from swift.common.swob import Request, Response
    
    class IPWhitelistMiddleware(object):
        """
        IP Whitelist Middleware
    
        Middleware that allows access to a container from only a set of IP
        addresses as determined by the container's metadata items that start
        with the prefix 'allow'. E.G. allow-dev=192.168.0.20
        """
    
        def __init__(self, app, conf, logger=None):
            self.app = app
    
            if logger:
                self.logger = logger
            else:
                self.logger = get_logger(conf, log_route='ip_whitelist')
    
            self.deny_message = conf.get('deny_message', "IP Denied")
            self.local_ip = socket.gethostbyname(socket.gethostname())
    
        def __call__(self, env, start_response):
            """
            WSGI entry point.
            Wraps env in swob.Request object and passes it down.
    
            :param env: WSGI environment dictionary
            :param start_response: WSGI callable
            """
            req = Request(env)
    
            try:
                version, account, container, obj = req.split_path(1, 4, True)
            except ValueError:
                return self.app(env, start_response)
    
            container_info = get_container_info(
                req.environ, self.app, swift_source='IPWhitelistMiddleware')
    
            remote_ip = env['REMOTE_ADDR']
            self.logger.debug("Remote IP: %(remote_ip)s",
                              {'remote_ip': remote_ip})
    
            meta = container_info['meta']
            allow = {k:v for k,v in meta.iteritems() if k.startswith('allow')}
            allow_ips = set(allow.values())
            allow_ips.add(self.local_ip)
            self.logger.debug("Allow IPs: %(allow_ips)s",
                              {'allow_ips': allow_ips})
    
            if remote_ip in allow_ips:
                return self.app(env, start_response)
            else:
                self.logger.debug(
                    "IP %(remote_ip)s denied access to Account=%(account)s "
                    "Container=%(container)s. Not in %(allow_ips)s", locals())
                return Response(
                    status=403,
                    body=self.deny_message,
                    request=req)(env, start_response)
    
    
    def filter_factory(global_conf, **local_conf):
        """
        paste.deploy app factory for creating WSGI proxy apps.
        """
        conf = global_conf.copy()
        conf.update(local_conf)
    
        def ip_whitelist(app):
            return IPWhitelistMiddleware(app, conf)
        return ip_whitelist
    

    envconf 中有很多有用的信息,您可以使用它们来决定如何处理请求。 要了解更多可用属性,您可以将以下日志语句插入到 __init__ 方法中

    self.logger.debug("conf = %(conf)s", locals())
    

    并将以下日志语句插入到 __call__ 方法中

    self.logger.debug("env = %(env)s", locals())
    
  4. 要将此中间件插入 swift Paste 管道,您需要编辑一个配置文件,/etc/swift/proxy-server.conf

    $ vim /etc/swift/proxy-server.conf
    
  5. /etc/swift/proxy-server.conf 中找到 [filter:ratelimit] 部分,并在其后复制以下配置部分

    [filter:ip_whitelist]
    paste.filter_factory = swift.common.middleware.ip_whitelist:filter_factory
    # You can override the default log routing for this filter here:
    # set log_name = ratelimit
    # set log_facility = LOG_LOCAL0
    # set log_level = INFO
    # set log_headers = False
    # set log_address = /dev/log
    deny_message = You shall not pass!
    
  6. /etc/swift/proxy-server.conf 中找到 [pipeline:main] 部分,并将 ip_whitelist 添加到 ratelimit 之后,如下所示。 完成后,保存并关闭文件

    [pipeline:main]
    pipeline = catch_errors gatekeeper healthcheck proxy-logging cache bulk tempurl ratelimit ip_whitelist ...
    
  7. 重新启动 swift proxy 服务,以使 swift 使用您的中间件。 首先切换到 swift-proxy screen

    1. 按下 Ctrl+A,然后按下 3

    2. 按下 Ctrl+C 以终止服务。

    3. 按下 Up Arrow 以调出上一个命令。

    4. 按下 Enter 以运行它。

  8. 使用 swift CLI 测试您的中间件。 首先切换到 shell screen,最后切换回 swift-proxy screen 以检查日志输出

    1. 按下 Ctrl+A,然后按下 0

    2. 确保您位于 devstack 目录中

      $ cd /root/devstack
      
    3. source openrc 以设置 CLI 的环境变量

      $ . openrc
      
    4. 创建一个名为 middleware-test 的容器

      $ swift post middleware-test
      
    5. 按下 Ctrl+A,然后按下 3 以检查日志输出。

  9. 在日志语句中,您将看到以下行

    proxy-server Remote IP: my.instance.ip.address (txn: ...)
    proxy-server Allow IPs: set(['my.instance.ip.address']) (txn: ...)
    

    这两条语句由我们的中间件生成,表明请求是从我们的 DevStack 实例发送的,并且被允许了。

  10. 从可以访问您的 DevStack 实例的远程机器上的 DevStack 外部测试中间件

    1. 在您的本地机器上安装 keystoneswift 客户端

      # pip install python-keystoneclient python-swiftclient
      
    2. 尝试列出 middleware-test 容器中的对象

      $ swift --os-auth-url=http://my.instance.ip.address:5000/v2.0/ \
        --os-region-name=RegionOne --os-username=demo:demo \
        --os-password=devstack list middleware-test
      Container GET failed: http://my.instance.ip.address:8080/v1/AUTH_.../
          middleware-test?format=json 403 Forbidden   You shall not pass!
      
  11. 按下 Ctrl+A,然后按下 3 以检查日志输出。 再次查看 swift 日志语句,您将看到以下行

    proxy-server Authorizing from an overriding middleware (i.e: tempurl) (txn: ...)
    proxy-server ... IPWhitelistMiddleware
    proxy-server Remote IP: my.local.ip.address (txn: ...)
    proxy-server Allow IPs: set(['my.instance.ip.address']) (txn: ...)
    proxy-server IP my.local.ip.address denied access to Account=AUTH_... \
       Container=None. Not in set(['my.instance.ip.address']) (txn: ...)
    

    在这里我们可以看到,由于远程 IP 地址不在允许的 IP 集合中,请求被拒绝了。

  12. 在您的 DevStack 实例的 shell screen 中,为容器添加一些元数据,以允许来自远程机器的请求

    1. 按下 Ctrl+A,然后按下 0

    2. 添加元数据以允许 IP

      $ swift post --meta allow-dev:my.local.ip.address middleware-test
      
    3. 现在再次尝试步骤 10 中的命令,它会成功。 容器中没有对象,因此没有要列出的内容;但是,也没有错误报告。

      警告

      像这样的功能测试不能替代适当的单元和集成测试,但它可以帮助您入门。

您可以遵循其他使用 Python Paste 框架的项目中的类似模式。 仅创建中间件模块并通过配置将其插入。 中间件按顺序运行,作为该项目管道的一部分,并可以根据需要调用其他服务。 不会触碰任何项目核心代码。 在 /etc/<project> 中的项目的 confini 配置文件中查找 pipeline 值,以识别使用 Paste 的项目。

当您的中间件完成后,我们鼓励您将其开源并在 OpenStack 邮件列表中告知社区。 也许其他人需要相同的功能。 他们可以使用您的代码、提供反馈并可能做出贡献。 如果对其存在足够的支持,也许您可以建议将其添加到官方 swift middleware