Apache SetEnv 无法与 mod_wsgi 一起按预期工作

Posted

技术标签:

【中文标题】Apache SetEnv 无法与 mod_wsgi 一起按预期工作【英文标题】:Apache SetEnv not working as expected with mod_wsgi 【发布时间】:2012-02-19 10:49:38 【问题描述】:

在我编写的烧瓶应用程序中,我使用了一个可以使用环境变量进行配置的外部库。注意:我自己编写了这个外部库。所以我可以在必要时进行更改。当从命令行运行烧瓶服务器时:

# env = python virtual environment
ENV_VAR=foo ./env/bin/python myapp/webui.py

一切正常。但是在将其部署到 apache 并使用 SetEnv 之后,它确实不再工作了。事实上,将os.environ 打印到stderr(所以它显示在 apache 日志中显示,wsgi 进程似乎处于一个非常不同的环境中(例如,os.environ['PWD'] 似乎是 方式。事实上,它指向我的开发文件夹。

为了帮助识别问题,以下是作为独立 hello-world 应用程序的应用程序的相关部分。错误输出和观察结果在帖子的最后。

应用文件夹布局:

Python 应用程序:

.
├── myapp.ini
├── setup.py
└── testenv
    ├── __init__.py
    ├── model
    │   └── __init__.py
    └── webui.py

Apache 文件夹 (/var/www/michel/testenv):

.
├── env
│   ├── [...]
├── logs
│   ├── access.log
│   └── error.log
└── wsgi
└── app.wsgi

myapp.ini

[app]
somevar=somevalue

setup.py

from setuptools import setup, find_packages

setup(
    name="testenv",
    version='1.0dev1',
    description="A test app",
    long_description="Hello World!",
    author="Some Author",
    author_email="author@example.com",
    license="BSD",
    include_package_data=True,
    install_requires = [
      'flask',
      ],
    packages=find_packages(exclude=["tests.*", "tests"]),
    zip_safe=False,
)

testenv/init.py

# empty

testenv/model/init.py

from os.path import expanduser, join, exists
from os import getcwd, getenv, pathsep
import logging
import sys

__version__ = '1.0dev1'

LOG = logging.getLogger(__name__)

def find_config():
    """
    Searches for an appropriate config file. If found, return the filename, and
    the parsed search path
    """

    path = [getcwd(), expanduser('~/.mycompany/myapp'), '/etc/mycompany/myapp']
    env_path = getenv("MYAPP_PATH")
    config_filename = getenv("MYAPP_CONFIG", "myapp.ini")
    if env_path:
        path = env_path.split(pathsep)

    detected_conf = None
    for dir in path:
        conf_name = join(dir, config_filename)
        if exists(conf_name):
            detected_conf = conf_name
            break
    return detected_conf, path

def load_config():
    """
    Load the config file.
    Raises an OSError if no file was found.
    """
    from ConfigParser import SafeConfigParser

    conf, path = find_config()
    if not conf:
        raise OSError("No config file found! Search path was %r" % path)

    parser = SafeConfigParser()
    parser.read(conf)
    LOG.info("Loaded settings from %r" % conf)
    return parser

try:
    CONF = load_config()
except OSError, ex:
    # Give a helpful message instead of a scary stack-trace
    print >>sys.stderr, str(ex)
    sys.exit(1)

testenv/webui.py

from testenv.model import CONF

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return "Hello World %s!" % CONF.get('app', 'somevar')

if __name__ == '__main__':
    app.debue = True
    app.run()

Apache 配置

<VirtualHost *:80>
    ServerName testenv-test.my.fq.dn
    ServerAlias testenv-test

    WSGIDaemonProcess testenv user=michel threads=5
    WSGIScriptAlias / /var/www/michel/testenv/wsgi/app.wsgi
    SetEnv MYAPP_PATH /var/www/michel/testenv/config

    <Directory /var/www/michel/testenv/wsgi>
        WSGIProcessGroup testenv
        WSGIApplicationGroup %GLOBAL
        Order deny,allow
        Allow from all
    </Directory>

    ErrorLog /var/www/michel/testenv/logs/error.log
    LogLevel warn

    CustomLog /var/www/michel/testenv/logs/access.log combined

</VirtualHost>

app.wsgi

activate_this = '/var/www/michel/testenv/env/bin/activate_this.py'
execfile(activate_this, dict(__file__=activate_this))

from os import getcwd
import logging, sys

from testenv.webui import app as application

# You may want to change this if you are using another logging setup
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)

LOG = logging.getLogger(__name__)
LOG.debug('Current path: 0'.format(getcwd()))

# Application config
application.debug = False

# vim: set ft=python :

错误和观察

这是 apache 错误日志的输出。

[Thu Jan 26 10:48:15 2012] [error] No config file found! Search path was ['/home/users/michel', '/home/users/michel/.mycompany/myapp', '/etc/mycompany/myapp']
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101] mod_wsgi (pid=17946): Target WSGI script '/var/www/michel/testenv/wsgi/app.wsgi' cannot be loaded as Python module.
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101] mod_wsgi (pid=17946): SystemExit exception raised by WSGI script '/var/www/michel/testenv/wsgi/app.wsgi' ignored.
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101] Traceback (most recent call last):
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101]   File "/var/www/michel/testenv/wsgi/app.wsgi", line 10, in <module>
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101]     from testenv.webui import app as application
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101]   File "/var/www/michel/testenv/env/lib/python2.6/site-packages/testenv-1.0dev1-py2.6.egg/testenv/webui.py", line 1, in <module>
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101]     from testenv.model import CONF
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101]   File "/var/www/michel/testenv/env/lib/python2.6/site-packages/testenv-1.0dev1-py2.6.egg/testenv/model/__init__.py", line 51, in <module>
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101]     sys.exit(1)
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101] SystemExit: 1

我的第一个观察是环境变量MYAPP_PATH 没有出现在os.environ 中(这在这个输出中是不可见的,但是我测试了它,它不存在!)。因此,配置“解析器”回退到默认路径。

我的第二个观察是配置文件的搜索路径将/home/users/michel 列为os.getcwd() 的返回值。我实际上是在期待 /var/www/michel/testenv 里面的东西。

我的直觉告诉我,我做配置解析的方式是不对的。主要是因为代码是在导入时执行的。这让我想到,也许配置解析代码在正确设置 WSGI 环境之前执行。我有什么东西吗?

简短讨论/切题

在这种情况下,将如何进行配置解析?鉴于“model”子文件夹实际上是一个外部模块,它也应该在非 wsgi 应用程序中工作,并且应该提供一种配置数据库连接的方法。

就我个人而言,我喜欢在搜索配置文件的同时仍然能够覆盖它的方式。只是,代码在导入时执行的事实让我的蜘蛛感觉像疯了一样刺痛。这背后的基本原理:使用此模块的其他开发人员完全隐藏了配置处理(抽象障碍),并且它“正常工作”。他们只需要导入模块(当然是现有的配置文件)并且可以在不知道任何数据库细节的情况下直接进入。这也使他们能够轻松地使用不同的数据库(开发/测试/部署)并在它们之间轻松切换。

现在,在 mod_wsgi 内部它不再存在了 :(

更新:

刚才,为了测试我的上述想法,我将webui.py更改为以下内容:

import os

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def index():
    return jsonify(os.environ)

if __name__ == '__main__':
    app.debue = True
    app.run()

网页上的输出如下:


    LANG: "C",
    APACHE_RUN_USER: "www-data",
    APACHE_PID_FILE: "/var/run/apache2.pid",
    PWD: "/home/users/michel/tmp/testenv",
    APACHE_RUN_GROUP: "www-data",
    PATH: "/usr/local/bin:/usr/bin:/bin",
    HOME: "/home/users/michel/"

这显示了与其他调试方法看到的环境相同的环境。所以我最初的想法是错误的。但现在我意识到了一些奇怪的事情。 os.environment['PWD'] 设置为我的开发文件所在的文件夹。这并不是应用程序运行的全部。更奇怪的是,os.getcwd() 返回/home/users/michel?这与我在os.environ 中看到的不一致。不应该和os.environ['PWD']一样吗?

但最重要的问题仍然存在:为什么 apache 的 SetEnv(在本例中为 MYAPP_PATH)设置的值在 os.environ 中找不到?

【问题讨论】:

+1 - 优秀 问题。 看起来你写这个问题比在你的实际应用上做的工作更多。写得很好:) @Cerin:我认为这对其他人有帮助。所以我尽量把我的问题写得尽可能详细。也许有人从问题答案中学到了一些东西^_^ 【参考方案1】:

请注意,WSGI 环境会在应用程序对象的environ 参数中的每个请求中传递给应用程序。该环境与os.environ 中保存的进程环境完全无关。 SetEnv 指令对os.environ 没有影响,并且无法通过 Apache 配置指令影响进程环境中的内容。

因此,您必须执行除 getenvironos.environ['PWD'] 之外的其他操作才能从 apache 获取 MY_PATH

Flask 将 wsgi 环境添加到请求中,而不是 app.environ,它是由底层 werkzeug 完成的。因此,在对应用程序的每个请求中,apache 都会添加 MYAPP_CONF 键,并且您可以在任何可以访问请求的地方访问它,例如 request.environ.get('MYAPP_CONFIG')

【讨论】:

【参考方案2】:

@rapadura 的答案是正确的,因为您无法直接访问 Apache 配置中的 SetEnv 值,但您可以解决它。

如果您在app.wsgi 文件中为application 添加包装器,则可以在每个请求上设置os.environ。示例见以下修改后的app.wsgi

activate_this = '/var/www/michel/testenv/env/bin/activate_this.py'
execfile(activate_this, dict(__file__=activate_this))

from os import environ, getcwd
import logging, sys

from testenv.webui import app as _application

# You may want to change this if you are using another logging setup
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)

LOG = logging.getLogger(__name__)
LOG.debug('Current path: 0'.format(getcwd()))

# Application config
_application.debug = False

def application(req_environ, start_response):
    environ['MYAPP_CONF'] = req_environ['MYAPP_CONF']
    return _application(req_environ, start_response)

如果您在 Apache 配置中设置了更多环境变量,则需要在 application 包装函数中显式设置每个环境变量。

【讨论】:

我对变量名称使用约定,例如 MYAPP_FOO、MYAPP_BAR,然后在包装器中我可以使用循环。适用于具有许多配置选项的应用程序。 _env = k.replace('MYAPP_',''): v for k,v in req_environ.items() if k.startswith('MYAPP_')os.environ.update(_env) 或者你可以把变量放在.wsgi文件中而不是apache配置中。 @BenWhaley .wsgi 文件通常与应用程序本身一起存储在版本控制系统中,这意味着它不适用于“秘密”或对其所在环境唯一的配置值运行(例如 db/test/prod 具有不同的数据库用户名/密码)。使用环境变量意味着您可以在每个环境(dev/test/prod)中检查相同的代码,并假设环境变量设置正确,它应该可以运行。这就是为什么它们存储在 apache 配置中,而不是 .wsgi 文件中。 我认为这取决于你如何管理你的配置。如果您有配置管理解决方案,则 wsgi 文件可以是模板,而 CM 工具只需填写变量。关键是有很多方法可以解决这个问题。 @BenWhaley 大多数 PAAS 都假设您正在使用环境变量,这就是为什么它已成为规范的原因。如果您真的想使用某种模板(我不建议这样做),那么配置值最好与您的其他配置值一起存储(即处理 Flask app.config 的地方)而不是在 @987654333 @文件。

以上是关于Apache SetEnv 无法与 mod_wsgi 一起按预期工作的主要内容,如果未能解决你的问题,请参考以下文章

Django + mod_wsgi。从 Apache 的 SetEnv 设置操作系统环境变量

允许 cPanel 管理 SetEnv 参数

找不到Tomcat 7 setenv.sh

在python脚本中运行setenv linux命令[重复]

在 linux 中使用 apache 和 mod_wsgi 配置 Python flask 应用程序

内部libc函数调用必须通过.plt