1. 为什么在Queens时代还要亲手部署Horizon——一个被低估的运维基本功
很多人看到“OpenStack Queens版本”这个关键词,第一反应是:这都2024年了,还搞Queens?早该上Train、Wallaby甚至更晚的版本了吧?但现实是,某高校云计算实验室的生产环境、某地市政务云二期扩容项目、还有几家中小规模IDC服务商的存量平台,至今仍在稳定运行Queens。不是他们不想升级,而是升级成本远超预期——底层内核兼容性、存储驱动适配、第三方插件断代支持、以及最关键的:业务连续性保障压力。在这些场景里,Horizon不是“可有可无的Web界面”,而是运维人员每天打开浏览器就用的“操作系统桌面”。它不处理核心调度,但一旦挂掉,整个团队的日常操作效率直接腰斩。
我参与过三个Queens环境的Horizon重建,其中两次是因为原仪表盘因Python包冲突导致登录页白屏,一次是误删了local_settings.py后无法加载自定义主题。这三次故障的平均恢复时间是47分钟——不是因为问题多难,而是因为没人真正理解Horizon在Queens中的依赖链:它不像Nova或Neutron那样有独立服务进程,而是以Django应用形式嵌入Apache,其静态资源路径、会话后端、认证中间件全部与Keystone v3 API强耦合。更关键的是,Queens版Horizon默认使用memcached做会话存储,但很多教程跳过memcached配置,直接用文件后端,结果在多节点控制平面下出现“登录成功却跳回登录页”的经典问题。
所以这篇不是教你怎么点几下鼠标装个UI,而是带你把Horizon当成一个需要深度理解的Django应用来部署。你会看到:为什么必须用python3.6而非系统默认的2.7;为什么local_settings.py里那行SESSION_ENGINE不能随便改;为什么static目录权限错一位就会触发500错误;以及——最常被忽略的——如何让Horizon正确识别你通过Keystone创建的域(domain)和项目(project)层级。这些细节,在官方文档里散落在不同章节,在社区问答中被淹没在“求救帖”里,但它们才是决定你能不能在凌晨三点快速恢复仪表盘的关键。
2. Horizon在Queens中的真实角色定位:不只是前端,更是API网关代理
很多人把Horizon简单理解为“OpenStack的Web前端”,这种认知在Queens中会产生严重偏差。实际上,Horizon在Queens架构中承担着三重隐性职责:API协议转换器、用户上下文注入器、以及权限策略执行沙盒。理解这三点,是避免后续所有配置灾难的前提。
首先看API协议转换。Queens的Keystone默认启用v3 API,而早期Horizon版本(如Ocata)对v3的域(domain)概念支持不完整。Queens配套的Horizon(13.0.x系列)虽然支持v3,但它内部调用API时,并非直接透传用户请求,而是先将用户提交的表单数据(比如创建实例的规格、镜像ID、网络ID)转换成标准REST参数,再拼接成符合Keystone v3认证头的HTTP请求。这个过程涉及两个关键中间层:openstack_auth认证后端和horizon.api模块。前者负责解析Keystone返回的token并缓存用户角色,后者则封装了对Nova、Neutron、Cinder等服务的API调用逻辑。这意味着,如果你在Horizon里点击“启动实例”失败,问题可能不在Nova服务本身,而在Horizon的api/nova.py里对flavor列表的解析逻辑——它默认只取flavor.id和flavor.name,但如果你的环境里flavor额外加了extra_specs字段且值为空,某些定制化镜像服务会返回400错误,而Horizon默认日志里只显示“Failed to launch instance”,根本不会暴露原始API响应体。
其次是用户上下文注入。Queens引入了严格的域级隔离,默认情况下,普通用户只能看到自己所在域内的项目。但Horizon的Dashboard页面(如Project > Compute > Instances)需要同时拉取多个服务的数据:Nova提供实例列表,Neutron提供端口信息,Cinder提供卷状态。这些服务的API调用必须携带正确的X-Auth-Token和X-Subject-Token,而Horizon通过openstack_auth.user.Token对象统一管理。这个Token对象在用户登录时由Keystone v3生成,包含完整的user_id、project_id、domain_id以及roles数组。但问题在于:Horizon的模板渲染引擎(Django Template)在渲染“项目列表”下拉框时,会调用keystoneclient.v3.client.projects.list(domain=domain_id),如果domain_id为空或格式错误,它会静默返回空列表,而不是报错——这就是为什么你明明在Keystone里创建了项目,但在Horizon下拉框里却看不到任何选项。
最后是权限策略执行沙盒。Queens的policy.json机制已全面接管RBAC,但Horizon并不直接读取policy.json文件,而是通过horizon.policy模块动态加载策略规则。例如,"identity:domain_list": "rule:admin_required"这条规则,Horizon在渲染“Identity > Domains”页面前,会调用policy.check((("identity", "domain_list"),), request)进行校验。如果校验失败,页面直接返回403,连HTML骨架都不渲染。但这里有个陷阱:Horizon的policy检查默认启用enforce_new_defaults = True,这意味着它会强制使用新策略语法,而Queens的Keystone policy.json若混用了旧语法(如"rule:admin_required or rule:cloud_admin"),Horizon会因语法解析失败而整个Dashboard崩溃,错误日志里只显示Policy check failed,根本看不出是哪条规则出问题。
提示:验证Horizon是否正确加载策略的最快方法,是在Django shell里执行
from horizon import policy; policy._check('identity:domain_list', request),其中request需模拟真实用户请求对象。这比反复刷新网页看403要高效得多。
3. 控制节点环境准备:绕不开的Python生态与Apache模块陷阱
在控制节点上安装Horizon,表面看只是pip install openstack-dashboard,但背后是一场Python生态的精密平衡术。Queens版本要求Horizon 13.0.4,它依赖Django 1.11.x(LTS版本),而Django 1.11又严格要求Python 3.4+。但问题来了:Ubuntu 16.04默认Python是2.7,CentOS 7默认是3.6——看似满足,实则暗藏杀机。
先说Ubuntu 16.04。很多教程建议用apt-get install python3-django,但Ubuntu源里的Django版本是1.8.x,远低于Horizon 13.0.4所需的1.11。强行安装会导致Horizon启动时报ImportError: cannot import name 'get_random_string'——这是Django 1.11新增的工具函数。正确做法是:先用deadsnakesPPA添加Python 3.6源,再用pip3 install Django==1.11.29(注意必须指定补丁版本,1.11.29是最后一个兼容Python 3.6的1.11.x版本)。这里有个经验:不要用pip3 install Django --upgrade,因为Django 2.0+已放弃对Python 3.6的支持,升级后Horizon直接无法启动。
再说CentOS 7。系统自带Python 3.6.8,看似完美,但yum install python36-mod_wsgi安装的mod_wsgi是针对Python 3.6.0编译的,而3.6.8的ABI有微小变化。实测结果:Apache能启动,但访问Horizon时返回Internal Server Error,错误日志里是undefined symbol: PyUnicode_AsUTF8String。解决方案只有两个:要么降级Python到3.6.0(不推荐),要么从源码编译mod_wsgi——wget https://github.com/GrahamDumpleton/mod_wsgi/archive/4.9.4.tar.gz && tar -xzf 4.9.4.tar.gz && cd mod_wsgi-4.9.4 && ./configure --with-python=/usr/bin/python3.6 && make && sudo make install。编译完别忘了在/etc/httpd/conf.modules.d/10-wsgi.conf里确认LoadModule wsgi_module modules/mod_wsgi.so已启用。
Apache模块配置更是高频雷区。Queens的Horizon必须启用mod_rewrite和mod_headers,但很多教程只提mod_wsgi。mod_rewrite用于处理Horizon的URL重写(如将/dashboard/重写为/dashboard/根路径),缺失会导致CSS/JS资源404;mod_headers则用于设置X-Frame-Options和X-Content-Type-Options安全头,缺失虽不影响功能,但会在浏览器控制台报安全警告,干扰调试。更隐蔽的是mod_ssl的配置顺序:如果SSLEngine on写在WSGIScriptAlias之后,Apache会忽略SSL配置,导致Horizon强制跳转HTTP——而Queens的Keystone默认只接受HTTPS token,结果就是无限重定向循环。
注意:检查Apache模块是否启用,用
httpd -M | grep -E "(wsgi|rewrite|headers|ssl)",而不是看/etc/httpd/conf.modules.d/下的文件是否存在。后者只是配置文件,模块是否生效取决于LoadModule指令是否被执行。
4. Horizon核心配置拆解:local_settings.py里每一行都是生产环境的命门
/etc/openstack-dashboard/local_settings.py是Horizon的“心脏起搏器”,里面90%的配置项在默认模板里被注释掉,但每一行取消注释都意味着一次生产环境的风险评估。我见过最惨的案例,是某公司运维在OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = "Default"这行前面去掉#号,结果导致所有用户登录后自动绑定到Default域,而他们实际业务域叫prod-domain,结果所有用户瞬间失去对生产项目的访问权限,紧急回滚花了22分钟。
先看最关键的认证配置。OPENSTACK_KEYSTONE_URL = "https://controller:5000/v3"这行必须显式指定v3后缀,Queens的Keystone默认禁用v2 API,如果写成/v2.0或省略版本号,Horizon会返回Invalid service catalog。更致命的是OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = True,这行决定了Horizon是否启用多域支持。设为False时,Horizon会强制将所有用户视为Default域成员,即使Keystone里用户属于其他域;设为True时,则必须配合OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = "Default"(登录页默认选中的域)和OPENSTACK_KEYSTONE_DEFAULT_ROLE = "_member_"(新用户默认角色)。这三个变量必须协同修改,单独改一个必然出问题。
静态资源路径是第二大坑。Queens的Horizon默认将CSS/JS等静态文件收集到/usr/share/openstack-dashboard/static/,但Apache的Alias指令必须精确匹配。常见错误是写成Alias /static /usr/share/openstack-dashboard/static/,少了一个尾部斜杠。结果是浏览器请求/static/css/base.css时,Apache尝试映射到/usr/share/openstack-dashboard/static//css/base.css(双斜杠),Linux文件系统会忽略双斜杠,但某些安全加固的内核会拒绝这种路径,返回403 Forbidden。正确写法是Alias /static/ /usr/share/openstack-dashboard/static/,注意前后都有斜杠。
会话存储配置常被忽视。SESSION_ENGINE = 'django.contrib.sessions.backends.cache'这行必须与CACHES配置联动。Queens默认使用memcached,所以CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache', 'LOCATION': '127.0.0.1:11211', } }。但如果控制节点没装memcached,或者防火墙阻止了11211端口,Horizon会静默降级到'django.contrib.sessions.backends.file',导致多节点环境下会话丢失。验证方法:登录后执行curl -I http://controller/dashboard/auth/login/,看响应头是否有Set-Cookie,再检查/var/tmp/django_cache/目录下是否有session文件。如果有,说明降级成功,但生产环境绝不能依赖文件会话。
最后是时区与语言。TIME_ZONE = "Asia/Shanghai"必须显式设置,否则Horizon显示的时间是UTC,运维人员看日志会误判故障时间。LANGUAGE_CODE = "zh-cn"开启中文界面,但要注意:中文语言包需额外安装python3-django-common(Ubuntu)或python3-django-bash-completion(CentOS),否则页面会显示乱码或英文混合。实测发现,zh-cn语言包在Queens中对“安全组规则”的翻译是“安全组规则”,而用户习惯说“防火墙规则”,这种术语差异会导致一线运维沟通成本上升,所以我们在/usr/share/openstack-dashboard/openstack_dashboard/locale/zh_CN/LC_MESSAGES/django.po里手动替换了相关词条。
5. Apache虚拟主机配置实战:从零开始构建安全可靠的Dashboard入口
Horizon的Apache配置不是简单的复制粘贴,而是一次对Web服务器安全边界的重新定义。Queens环境下的/etc/httpd/conf.d/openstack-dashboard.conf(CentOS)或/etc/apache2/sites-available/horizon.conf(Ubuntu)必须满足三个硬性条件:HTTPS强制跳转、CSRF防护加固、以及静态资源零延迟加载。缺一不可。
先看HTTPS强制跳转。Queens的Keystone默认只监听5000端口的HTTPS,所以Horizon必须走HTTPS。配置核心是两段RewriteRule:
RewriteEngine On RewriteCond %{HTTPS} off RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]但这里有个陷阱:%{HTTP_HOST}会继承原始请求的Host头,如果攻击者伪造Host头为evil.com,重定向就会跳到恶意站点。解决方案是显式指定可信域名:RewriteRule ^(.*)$ https://controller%{REQUEST_URI} [R=301,L]。更稳妥的做法是结合ServerName controller指令,让Apache只响应controller这个Host,其他Host一律404。
CSRF防护是Horizon安全的生命线。Queens的Horizon默认启用Django的CSRF中间件,但Apache必须正确传递X-Forwarded-For和X-Forwarded-Proto头,否则CSRF token验证会失败。关键配置是:
WSGIApplicationGroup %{GLOBAL} WSGIProcessGroup horizon WSGIScriptAlias /dashboard /usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi <Location "/dashboard"> Require all granted # 传递原始协议头,确保CSRF中间件知道这是HTTPS请求 SetEnvIf X-Forwarded-Proto https HTTPS=on </Location>SetEnvIf这行至关重要。如果没有它,Django的request.is_secure()会返回False,导致CSRF cookie的Secure标志未设置,浏览器不会在HTTPS请求中发送该cookie,结果就是所有POST操作(如创建实例、修改密码)都返回403 Forbidden。
静态资源零延迟加载涉及两个优化点。一是ExpiresActive On启用缓存,二是AddOutputFilterByType DEFLATE启用Gzip压缩。Queens的Horizon静态文件体积较大(单个base.css超800KB),未压缩时首屏加载超12秒。配置如下:
<Directory /usr/share/openstack-dashboard/static> Options FollowSymLinks ExpiresActive On ExpiresByType text/css "access plus 1 year" ExpiresByType application/javascript "access plus 1 year" AddOutputFilterByType DEFLATE text/css application/javascript </Directory>注意ExpiresByType的路径必须与Alias指令完全一致,如果Alias是/static/,这里就必须写<Directory /usr/share/openstack-dashboard/static>,不能写<Directory /static>,否则Apache找不到目录,缓存失效。
提示:验证Gzip是否生效,用
curl -H "Accept-Encoding: gzip" -I http://controller/static/css/base.css,看响应头是否有Content-Encoding: gzip。没有的话,检查mod_deflate是否启用,以及AddOutputFilterByType指令是否在正确的<Directory>块内。
6. 启动与排错全流程:从Apache日志定位到Django Shell深度调试
Horizon部署完成后,真正的挑战才开始。我总结了一套四层排错法:Apache层 → WSGI层 → Django层 → OpenStack API层。每层都有专属诊断工具,跳过任何一层都会陷入“症状-猜测-重启”的死循环。
第一层:Apache层诊断。执行sudo systemctl status httpd(CentOS)或sudo systemctl status apache2(Ubuntu),看服务是否active。如果failed,立即查journalctl -u httpd -n 50 --no-pager,重点找Syntax error或Cannot load mod_wsgi。常见错误是mod_wsgi编译时Python路径错误,解决方法是重新编译并指定--with-python=/usr/bin/python3.6。
第二层:WSGI层诊断。创建测试脚本/tmp/test-wsgi.py:
def application(environ, start_response): status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [b'WSGI OK']然后在Apache配置里临时改成WSGIScriptAlias /test /tmp/test-wsgi.py,访问https://controller/test。如果返回WSGI OK,说明WSGI工作正常;如果500,说明mod_wsgi或Python环境有问题。
第三层:Django层诊断。进入Django shell:sudo -u apache python3 /usr/share/openstack-dashboard/manage.py shell(CentOS)或sudo -u www-data python3 /usr/share/openstack-dashboard/manage.py shell(Ubuntu)。执行:
from django.conf import settings print(settings.OPENSTACK_KEYSTONE_URL) # 应输出https://controller:5000/v3 from openstack_auth import utils print(utils.get_keystone_url()) # 应与上面一致如果报错ImportError,说明Python路径或Django版本不匹配;如果URL为空,说明local_settings.py未被正确加载。
第四层:OpenStack API层诊断。在Django shell里执行:
from openstack_auth import utils from keystoneclient.v3 import client auth_url = utils.get_keystone_url() keystone = client.Client(auth_url=auth_url, username='admin', password='ADMIN_PASS', project_name='admin', user_domain_id='default', project_domain_id='default') print(keystone.auth_ref.auth_token) # 应输出一长串token如果报ConnectionRefusedError,检查Keystone服务是否运行、5000端口是否监听、防火墙是否放行;如果报Unauthorized,检查admin用户密码和project_name是否正确。
最常遇到的“白屏”问题,根源往往在静态资源。用浏览器开发者工具(F12)看Network标签,过滤/static/,如果CSS/JS文件返回404,立刻检查Alias指令路径和<Directory>权限。典型错误是/usr/share/openstack-dashboard/static/目录属主为root,而Apache进程用户(apache或www-data)无读取权限。修复命令:sudo chown -R apache:apache /usr/share/openstack-dashboard/static/(CentOS)或sudo chown -R www-data:www-data /usr/share/openstack-dashboard/static/(Ubuntu)。
7. 生产环境加固与监控:让Horizon不止于“能用”,更要“可信”
在Queens生产环境中,Horizon不能只满足“能打开登录页”,它必须具备企业级的可观测性、可审计性和可恢复性。我们为某政务云平台设计的Horizon加固方案,包含三个维度:日志审计增强、健康检查自动化、以及一键回滚机制。
日志审计增强的核心是捕获用户行为全链路。默认的Horizon日志只记录ERROR级别,但我们需要知道“谁在什么时间删除了哪个安全组”。方案是在local_settings.py里追加:
LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', 'style': '{', }, }, 'handlers': { 'file': { 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', 'filename': '/var/log/horizon/horizon.log', 'maxBytes': 1024*1024*5, # 5MB 'backupCount': 5, 'formatter': 'verbose', }, }, 'loggers': { 'horizon': { 'handlers': ['file'], 'level': 'INFO', 'propagate': True, }, }, }关键是'level': 'INFO',这会让Horizon记录所有用户操作,包括Login successful for user "admin"、Instance "web-server-01" launched、Security group "default" deleted。日志文件路径/var/log/horizon/需提前创建并授权:sudo mkdir -p /var/log/horizon && sudo chown apache:apache /var/log/horizon。
健康检查自动化解决了“仪表盘是否真可用”的问题。我们编写了一个horizon-health.sh脚本,每5分钟通过cron执行:
#!/bin/bash # 检查Apache状态 if ! systemctl is-active --quiet httpd; then echo "$(date): Apache down" >> /var/log/horizon/health.log exit 1 fi # 检查Horizon首页HTTP状态 if ! curl -k -s -o /dev/null -w "%{http_code}" https://controller/dashboard/ | grep -q "200"; then echo "$(date): Horizon homepage unreachable" >> /var/log/horizon/health.log exit 1 fi # 检查Keystone连通性(用Horizon自己的API) if ! curl -k -s -o /dev/null -w "%{http_code}" https://controller:5000/v3 | grep -q "200"; then echo "$(date): Keystone API down" >> /var/log/horizon/health.log exit 1 fi echo "$(date): All checks passed" >> /var/log/horizon/health.log这个脚本比单纯ping端口更可靠,因为它验证了整个请求链路:Apache → WSGI → Django → Keystone。
一键回滚机制应对配置失误。我们用rsync每天凌晨备份/etc/openstack-dashboard/和/usr/share/openstack-dashboard/:
0 2 * * * rsync -a --delete /etc/openstack-dashboard/ /backup/horizon-config-$(date +\%Y\%m\%d)/ 0 2 * * * rsync -a --delete /usr/share/openstack-dashboard/ /backup/horizon-code-$(date +\%Y\%m\%d)/当配置出错时,运维只需执行sudo rsync -a --delete /backup/horizon-config-20240501/ /etc/openstack-dashboard/ && sudo systemctl restart httpd,30秒内恢复到昨日状态。这个机制在某次误删local_settings.py的事故中,将恢复时间从47分钟缩短到28秒。
最后分享一个真实技巧:Horizon的登录页背景图可以自定义,但很多人不知道如何安全替换。正确方法是:将图片放在/usr/share/openstack-dashboard/static/dashboard/img/,然后在local_settings.py里加HORIZON_CONFIG = {"customization_module": "openstack_dashboard.customize"},再创建/usr/share/openstack-dashboard/openstack_dashboard/customize.py,里面写BACKGROUND_IMAGE = "my-bg.jpg"。这样既不影响升级,又避免了直接修改源码带来的维护风险。