一、写在前面
这篇文章主要介绍一下OpenStack Horizon — juno项目用户登录流程,Horizon对于请求的处理其实就是django对请求的处理,我在这里将通过对django请求处理机制以及horizon用户登录流程代码的解析,逐步开始horizon代码阅读之旅。由于能力和时间有限,错误之处在所难免,欢迎指正!
如果转载,请保留作者信息。
邮箱地址:jpzhang.ht@gmail.com
二、Django 请求处理流程
当你通过在浏览器里敲http://127.0.0.1:8000/hello/来访问Hello world消息的时候,Django在后台有些什么动作呢?
流程图:
所有均开始于setting文件。当你运行Python manage.py runserver,脚本将在于manage.py同一个目录下查找名为setting.py的文件。这个文件包含了所有有关这个Django项目的配置信息,均大写: TEMPLATE_DIRS , DATABASE_NAME , 等. 最重要的设置时ROOT_URLCONF,它将作为URLconf告诉Django在这个站点中那些Python的模块将被用到
还记得什么时候django-admin.py startproject创建文件settings.py和urls.py吗?自动创建的settings.py包含一个ROOT_URLCONF配置用 来指向自动产生的urls.py. 打开文件settings.py你将看到如下:
- ROOT_URLCONF = 'mysite.urls'
相对应的文件是mysite/urls.py
当访问 URL /hello/ 时,Django 根据 ROOT_URLCONF 的设置装载 URLconf 。 然后按顺序逐个匹配URLconf里的URLpatterns,直到找到一个匹配的。 当找到这个匹配 的URLpatterns就调用相关联的view函数,并把 HttpRequest 对象作为第一个参数。
总结一下:
进来的请求转入/hello/.
Django通过在ROOT_URLCONF配置来决定根URLconf.
Django在URLconf中的所有URL模式中,查找第一个匹配/hello/的条目。
如果找到匹配,将调用相应的视图函数
视图函数返回一个HttpResponse
Django转换HttpResponse为一个适合的HTTP response, 以Web page显示出来
三、Horizon用户登录流程
说明:在具体分析horizon用户注册源码,会涉及到Horizon Template模版加载机制,本篇博文不具体说明,关于Template内容放到下个专题,专门一篇博文进行说明。
言归正传,首先horizon本身基于Django实现,简单地说就是网站的架构,主要分析前端请求和后端处理。所以第一步就是用户发起访问请求,第二步是找到前端请求与后端处理的绑定,具体为此次动作由哪个函数来处理等等;第三步,horizon必然会调用openstack Keystone的API接口验证访问用户,这里只分析后端处理流程,止步于API调用,暂且不详细分析。
根据Django的框架结构,使用URLconf文件(https://docs.djangoproject.com/en/1.4/topics/http/urls/)进行链接请求和后端处理(view)的绑定,使用view进行后端处理,使用template进行页面渲染。 前端请求与后端处理URL映射绑定是有urls.py完成,Django对URL设置内容是放在urls.py文件内,项目urls.py指定是在项目的setting.py配置文件内,那么OpenStack Horizon项目在setting.py中 ROOT_URLCONF、 TEMPLATE_DIRS 配置如下:
代码路径:/usr/share/openstack-dashboard/openstack_dashboard/setting.py
- ROOT_URLCONF = ‘openstack_dashboard.urls’
- ……
-
- TEMPLATE_LOADERS = (
- 'django.template.loaders.filesystem.Loader',
- 'django.template.loaders.app_directories.Loader',
- 'horizon.loaders.TemplateLoader'
- )
- TEMPLATE_DIRS = (
- os.path.join(ROOT_PATH, 'templates'),
- )
openstack_dashboard.urls.py构成了整个horizon项目的前端请求与后端处理的映射,那么Horizon 是如何实现整个Dashboard用户登录的呢?
Horizon用户登录数据流图:
![](https://img-blog.csdn.net/20150330010150059?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMTUyMTAxOQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
用户发起访问Request请求,用户访问Horizon地址: http://172.16.17.63/
URL绑定:
-
-
-
-
-
-
- urlpatterns = patterns(
- '',
-
- url(r'^$', 'openstack_dashboard.views.splash', name='splash'),
-
- url(r'^auth/', include('openstack_auth.urls')),
-
-
- url(r'^api/', include('openstack_dashboard.api.rest.urls')),
-
-
- url(r'', include(horizon.urls)),
- )
后端处理:
根据前端请求的URL地址配置“url(r'^$', 'openstack_dashboard.views.splash', name=‘splash’),”由此得知后端处理函数:openstack_dashboard.views.splash()
地址:/usr/share/openstack-dashboard/openstack_dashboard/views.py
-
-
-
-
- @django.views.decorators.vary.vary_on_cookie
- def splash(request):
-
- if request.user.is_authenticated():
-
-
-
-
- response = shortcuts.redirect(horizon.get_user_home(request.user))
- else:
-
-
-
-
- form = forms.Login(request)
-
-
-
- response = shortcuts.render(request, 'auth/login.html', {'form': form})
- if 'logout_reason' in request.COOKIES:
- response.delete_cookie('logout_reason')
- return response
splash()根据用户Request请求的不同类型提供了两种处理方式,一种:用户访问Horizon Web管理平台的地址,当前并没有用户登录,splash()函数加载登录界面;第二种:当前的请求带上了用户(登录),splash()函数验证用户的合法性,重定向到内部调用处理返回的用户访问地址。
这里分别进行两种请求的分析:
第一种:用户访问Horizon
生成form登录表单: splash()-> form = forms.Login(request)返回horizon登录表单。
from openstack_auth import forms引入form表单
openstack_auth:OpenStack是一个可插拔的Django项目,openstack_auth用户认证后端与Django contrib.auth框架来验证,提供OpenStack Keystone身份用户API。
- class Login(django_auth_forms.AuthenticationForm):
-
- """继承自django.contrib.auth.forms.AuthenticationForm:基类用户进行身份验证。扩展此得到接受用户名/密码登录形式。用于登录用户的表单
- 通过提供域名,用户名和密码,处理Keystone认证。如果用户认证都有一个默认的项目组,该令牌将被自动作用于它们的默认项目。用户认证没有默认设置的项目,该认证后端将尝试范围从用户的指定项目返回的项目。范围内的第一个成功的项目将被退回。继承了基类“django.contrib.auth.forms.AuthenticationForm”以增加安全功能。
- “""
-
-
- region = forms.ChoiceField(label=_("Region"), required=False)
-
-
- username = forms.CharField(
- label=_("User Name"),
- widget=forms.TextInput(attrs={"autofocus": "autofocus"}))
-
-
- password = forms.CharField(label=_("Password"),
- widget=forms.PasswordInput(render_value=False))
-
- def __init__(self, *args, **kwargs):
- super(Login, self).__init__(*args, **kwargs)
- ''
-
-
-
-
-
- self.fields.keyOrder = ['username', 'password', 'region']
-
- if getattr(settings,
- 'OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT',
- False):
-
-
-
- self.fields['domain'] = forms.CharField(
- label=_("Domain"),
- required=True,
-
- widget=forms.TextInput(attrs={"autofocus": "autofocus"}))
-
- self.fields['username'].widget = forms.widgets.TextInput()
-
- self.fields.keyOrder = ['domain', 'username', 'password', 'region']
-
-
- self.fields['region'].choices = self.get_region_choices()
-
- if len(self.fields['region'].choices) == 1:
-
-
- self.fields['region'].initial = self.fields['region'].choices[0][0]
-
- self.fields['region'].widget = forms.widgets.HiddenInput()
- elif len(self.fields['region'].choices) > 1:
-
- self.fields['region'].initial = self.request.COOKIES.get(
- 'login_region')
-
- @staticmethod
- def get_region_choices():
-
- default_region = (settings.OPENSTACK_KEYSTONE_URL, "Default Region")
-
- regions = getattr(settings, 'AVAILABLE_REGIONS', [])
-
- if not regions:
-
- regions = [default_region]
- return regions
r
esponse = shortcuts.render(request, 'auth/login.html', {'form': form}):渲染auth/login.html表单模版,参数传递登录表单。
Template 模版路径:horizon/templates/login.html
<!-- 继承自horizon/templates/base.html-->
{% extends "base.html" %}
- {% load i18n %}
-
- {% block title %}{% trans "Login" %}{% endblock %}
-
- {% block body_id %}splash{% endblock %}
-
- {% block content %}
- <!-- 导入horizon/templates/auth/_login.html-->
- {% include 'auth/_login.html' %}
- {% endblock %}
最终把response返回给客户端,展示登陆页面。
第二种:用户登录Horizon
splash()函数判断当前访问用户是否登录,如果登录跳转到该用户权限范围内的默认页面:
- if request.user.is_authenticated():
- response=
- shortcuts.redirect(horizon.get_user_home(request.user))
request.user.is_authenticated():True,调用horizon.get_uesr_home()
地址:horizon.base.Size:get_user_home(self, user)
- def get_user_home(self, user):
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- user_home = self._conf['user_home']
- if user_home:
-
- if callable(user_home):
-
- return user_home(user)
- elif isinstance(user_home, basestring):
-
-
- if '/' in user_home:
- return user_home
- else:
-
- mod, func = user_home.rsplit(".", 1)
-
-
-
-
-
- return getattr(import_module(mod), func)(user)
-
- raise ValueError('The user_home setting must be either a string '
- 'or a callable object (e.g. a function).')
- else:
- return self.get_absolute_url()
setting.py文件定义了user_home参数值:
- HORIZON_CONFIG = {
- 'user_home': 'openstack_dashboard.views.get_user_home',
加载openstack_dashboard.views 执行 get_user_home()
return getattr(import_module(mod), func)(user)
获取当前用户登录之后跳转的URL地址openstack_dashboard.views.get_user_home():
-
- def get_user_home(user):
- dashboard = None
- if user.is_superuser:
-
- try:
- dashboard = horizon.get_dashboard('admin')
- except base.NotRegistered:
- pass
-
- if dashboard is None:
-
- dashboard = horizon.get_default_dashboard()
-
- return dashboard.get_absolute_url()
判断当前登录user是否是superuser,例如:admin不是superuser用户,当前因此执行是:dashboard = horizon.get_default_dashboard(),返回“~horizon.Dashboard”默认实例 ,
地址:horizon.base.Size:def get_default_dashboard(self):
- def get_default_dashboard(self):
-
-
-
-
-
-
- if self.default_dashboard:
-
-
-
- return self._registered(self.default_dashboard)
- elif len(self._registry):
- return self.get_dashboards()[0]
- else:
- raise NotRegistered("No dashboard modules have been registered.")
HORIZON_CONFIG中是否指定了default_dashboard的值:’default_dashboard': ‘project',调用self._registered()函数传入self.default_dashboard值‘project',从已注册的模块中查找project模块并返回该实例对象,
地址:horizon.base.Registry:def _registered(self, cls):
- def _registered(self, cls):
-
-
-
-
-
- if inspect.isclass(cls) and issubclass(cls, self._registerable_class):
-
-
- found = self._registry.get(cls, None)
- if found:
- return found
- else:
-
-
- for registered in self._registry.values():
- if registered.slug == cls:
- return registered
- class_name = self._registerable_class.__name__
- if hasattr(self, "_registered_with"):
- parent = self._registered_with._registerable_class.__name__
- raise NotRegistered('%(type)s with slug "%(slug)s" is not '
- 'registered with %(parent)s "%(name)s".'
- % {"type": class_name,
- "slug": cls,
- "parent": parent,
- "name": self.slug})
- else:
- slug = getattr(cls, "slug", cls)
- raise NotRegistered('%(type)s with slug "%(slug)s" is not '
- 'registered.' % {"type": class_name,
- "slug": slug})
返回 return <Dashboard:project>,openstack_dashboard.views.get_user_home()中得到返回的<Dashboard:project>对象,接着调用该对象的get_absolute_url()返回该模块的访问地址。
地址:horizon.base.Dashboard:def get_absolute_url(self)
- def get_absolute_url(self):
-
-
-
-
- try:
-
-
-
-
-
-
- return self._registered(self.default_panel).get_absolute_url()
- except Exception:
- LOG.exception("Error reversing absolute URL for %s." % self)
- raise
获取该Dashboard 默认Panel的URL地址,循环之前返回Dasboard过程,通过horizon.base.Registry:def _registered(self, cls)—> horizon.base.Size:def get_default_dashboard(self)返回注册表中默认的Panel对象,接着调用horizon.base.Panel:defget_absolute_url(self) 返回Panel访问的URL地址:例如:’/project/‘。函数通过调用关系层层返回’/project/‘值,最终返回到openstack_dashboard.views.py:splash(),重定向到/project/地址,显示Project模块内容,Project模块内容加载过程后续博文说明。
原文:http://blog.csdn.net/u011521019/article/details/44738657