Apache SetEnv在mod_wsgi中未按预期工作
在我写的一个Flask应用中,我使用了一个可以通过环境变量配置的外部库。注意:这个外部库是我自己写的,所以如果需要,我可以进行修改。当我在命令行运行Flask服务器时,使用以下命令:
# 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.environ['PWD']
被设置为我开发文件的文件夹,这根本不是应用程序运行的地方。更奇怪的是,os.getcwd()
返回/home/users/michel
?这与我在os.environ
中看到的内容不一致。难道它不应该与os.environ['PWD']
相同吗?
不过,最重要的问题仍然是:为什么Apache的SetEnv
设置的值(在这个例子中是MYAPP_PATH
)在os.environ
中找不到?
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
的包装函数中明确地设置每一个变量。
请注意,WSGI环境在每次请求应用时会通过应用对象的environ
参数传递给应用。这种环境和os.environ
中保存的进程环境完全没有关系。SetEnv
指令对os.environ
没有影响,而且通过Apache的配置指令无法改变进程环境中的内容。
所以,如果你想从Apache获取MY_PATH
,你需要做一些其他的事情,而不是使用getenviron
或os.environ['PWD']
。
Flask会将WSGI环境添加到请求中,而不是app.environ
,这是由底层的werkzeug
完成的。因此,在每次请求应用时,Apache会添加MYAPP_CONF
这个键,你可以在任何可以访问请求的地方获取它,比如使用request.environ.get('MYAPP_CONFIG')
。