前言
笔者已经搭建了一个openstack的开发环境,能够调试运行Horizon工程,在本地的目录结构如下:
/horizon
├── babel-django.cfg
├── babel-djangojs.cfg
├── base.py
├── browsers
├── conf
├── context_processors.py
├── contrib
├── CONTRIBUTING.rst
├── decorators.py
├── doc
├── exceptions.py
├── forms
├── hacking
├── HACKING.rst
├── horizon
├── __init__.py
├── karma.conf.js
├── LICENSE
├── loaders.py
├── locale
├── Makefile
├── management
├── manage.py
├── MANIFEST.in
├── messages.py
├── middleware.py
├── notifications.py
├── openstack_dashboard
├── package.json
├── README.rst
├── releasenotes
├── requirements.txt
├── run_tests.sh
├── setup.cfg
├── setup.py
├── site_urls.py
├── static
├── tables
├── tabs
├── templates
├── templatetags
├── test
├── test-requirements.txt
├── test-shim.js
├── themes.py
├── tools
├── tox.ini
├── utils
├── venv
├── version.py
├── views.py
└── workflows
其中venv路径下包含了本地运行horizon工程的python环境,运行时指定工程的解释器为venv中python。并且horizon工程能够运行需要keystone服务,在此已经配置了正常运行的keystone环境。代码分析是基于M版本的openstack-horizon工程。
urls
一般读工程代码我习惯性从url入手,这样方便找到对应连接的响应函数,horizon工程中的horizon目录下是内容是horizon提供的库,其dashboard的核心在openstack_dashboard目录下,其中的url文件就是整个dashboard的主url文件,中urlpatterns内容如下:
import horizon
urlpatterns = patterns(
'',
url(r'^$', 'openstack_dashboard.views.splash', name='splash'),
url(r'^api/', include('openstack_dashboard.api.rest.urls')),
url(r'', include(horizon.urls)),
)
for u in getattr(settings, 'AUTHENTICATION_URLS', ['openstack_auth.urls']):
urlpatterns += patterns(
'',
url(r'^auth/', include(u))
)
# Development static app and project media serving using the staticfiles app.
urlpatterns += staticfiles_urlpatterns()
# Convenience function for serving user-uploaded media during
# development. Only active if DEBUG==True and the URL prefix is a local
# path. Production media should NOT be served by Django.
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG:
urlpatterns += patterns(
'',
url(r'^500/$', 'django.views.defaults.server_error')
)
整个文件中的内容,在一些openstack-horizon源码分析的网站有介绍,此处不在赘述。但是打开文件并未发现login的入口,此处提供一种通过django工程url链接寻找代码入口的方法:
用工程manager.py文件打开shell终端(注意此处一定要是运行工程环境的python解释器路径):
root@ubuntu:~/horizon$ ./venv/bin/python manage.py shell
Python 2.7.6 (default, Jun 22 2015, 17:58:13)
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.core.urlresolvers import reverse, resolve
>>> func, args, kwargs = resolve(reverse('login'))
Could not process panel theme_preview: Dashboard with slug "developer" is not registered.
>>> func.__module__
'openstack_auth.views'
>>>
该文件是 ./venv/lib/python2.7/site-packages/openstack_auth下的views.py文件,所以可以明白openstack_dashboard下url.py中:
for u in getattr(settings, 'AUTHENTICATION_URLS', ['openstack_auth.urls']):
urlpatterns += patterns(
'',
url(r'^auth/', include(u))
)
就是将openstack_auth下的urls.py文件中的url包含进来,代码如下:
from django.conf.urls import url
from openstack_auth import utils
from openstack_auth import views
utils.patch_middleware_get_user()
urlpatterns = [
url(r"^login/$", views.login, name='login'),
url(r"^logout/$", views.logout, name='logout'),
url(r'^switch/(?P<tenant_id>[^/]+)/$', views.switch,
name='switch_tenants'),
url(r'^switch_services_region/(?P<region_name>[^/]+)/$',
views.switch_region,
name='switch_services_region')
]
if utils.is_websso_enabled():
urlpatterns.append(url(r"^websso/$", views.websso, name='websso'))
Login代码分析
上urls.py中login链接的响应函数代码如下:
@sensitive_post_parameters()
@csrf_protect
@never_cache
def login(request, template_name=None, extra_context=None, **kwargs):
"""Logs a user in using the :class:`~openstack_auth.forms.Login` form."""
# If the user enabled websso and selects default protocol
# from the dropdown, We need to redirect user to the websso url
if request.method == 'POST':
auth_type = request.POST.get('auth_type', 'credentials')
if utils.is_websso_enabled() and auth_type != 'credentials':
auth_url = request.POST.get('region')
url = utils.get_websso_url(request, auth_url, auth_type)
return shortcuts.redirect(url)
if not request.is_ajax():
# If the user is already authenticated, redirect them to the
# dashboard straight away, unless the 'next' parameter is set as it
# usually indicates requesting access to a page that requires different
# permissions.
if (request.user.is_authenticated() and
auth.REDIRECT_FIELD_NAME not in request.GET and
auth.REDIRECT_FIELD_NAME not in request.POST):
return shortcuts.redirect(settings.LOGIN_REDIRECT_URL)
# Get our initial region for the form.
initial = {}
current_region = request.session.get('region_endpoint', None)
requested_region = request.GET.get('region', None)
regions = dict(getattr(settings, "AVAILABLE_REGIONS", []))
if requested_region in regions and requested_region != current_region:
initial.update({'region': requested_region})
if request.method == "POST":
form = functional.curry(forms.Login)
else:
form = functional.curry(forms.Login, initial=initial)
if extra_context is None:
extra_context = {'redirect_field_name': auth.REDIRECT_FIELD_NAME}
if not template_name:
if request.is_ajax():
template_name = 'auth/_login.html'
extra_context['hide'] = True
else:
template_name = 'auth/login.html'
res = django_auth_views.login(request,
template_name=template_name,
authentication_form=form,
extra_context=extra_context,
**kwargs)
# Save the region in the cookie, this is used as the default
# selected region next time the Login form loads.
if request.method == "POST":
utils.set_response_cookie(res, 'login_region',
request.POST.get('region', ''))
utils.set_response_cookie(res, 'login_domain',
request.POST.get('domain', ''))
# Set the session data here because django's session key rotation
# will erase it if we set it earlier.
if request.user.is_authenticated():
auth_user.set_session_from_user(request, request.user)
regions = dict(forms.Login.get_region_choices())
region = request.user.endpoint
login_region = request.POST.get('region')
region_name = regions.get(login_region)
request.session['region_endpoint'] = region
request.session['region_name'] = region_name
return res
Openstack dashboard中链接都是做过login_required限定,即访问必须用户登陆才能进行。当未登陆访问时首先跳转到‘/auth/login/?next=/’, 当然参数next可能是其他值(例如你在/project 页面会话过期,需要重新登陆,此时next就是/project,成功登陆后下一跳地址)。
三个装饰器:
(1) @sensitive_post_parameters()
Debug时候用来跟踪某些敏感变量,时刻能够观察敏感变量中的值。
(2) @csrf_protect
Django csrf保护 https://docs.djangoproject.com/en/1.7/ref/contrib/csrf/
(3) @never_cache
禁止使用缓存视图 http://djangobook.py3k.cn/2.0/chapter15/
Login 页面的GET请求
此时request.method 为GET,request.user.is_authenticated()为False,可见程序运行主线为:
获取initial, regions等值 –> 指定form表单并用initial初始化 –> 指定下一跳参数名称为next –> 指定模板为auth/login.html –> 利用django_auth_views.login()函数返回请求结果。
login部分页面位于horizon包中的templates/auth下
auth
├── _description.html
├── _login_form.html
├── _login.html
├── login.html
├── _login_modal.html
├── _login_page.html
└── _splash.html
login.html —include—> _login.html —include—> _login_page.html —extend—> _login_form.html
利用django form表单反向生成登陆页面,完成GET请求显示登陆界面。
Login页面POST请求
登陆界面填写用户名密码后,点击提交即发送POST请求到auth/login/链接。此时request.method 为POST,用户名密码如果匹配则request.user.is_authenticated()为True,否则为False。结之前提供源码,此处理解应该并不复杂,不在赘述。