一、写在前面
这篇文章主要介绍一下Openstack Horizon — juno项目用户登录流程,Horizon对于请求的处理其实就是django对请求的处理,我在这里将通过对django请求处理机制以及horizon用户登录流程代码的解析,逐步开始horizon代码阅读之旅。由于能力和时间有限,错误之处在所难免,欢迎指正!
如果转载,请保留作者信息。
邮箱地址:[email protected]
二、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’#来指向Horizon的urls.py …… #加载的Template模版 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用户登录数据流图:
用户发起访问Request请求,用户访问Horizon地址: http://172.16.17.63/
URL绑定:
""" patterns() 函数并将返回结果保存到 urlpatterns 变量。patterns函数当前只有一个参数—一个空的字符串。 这个字符串可以被用来表示一个视图函数的通用前缀。 当前应该注意是 urlpatterns 变量, Django 期望能从setting.py->ROOT_URLCONF模块中找到它。 该变量定义了 URL 以及用于处理这些 URL 的代码之间的映射关系。 """ urlpatterns = patterns( '', # 匹配网站根目录的URL,映射到openstack_dashboard.views.splash视图 url(r'^$', 'openstack_dashboard.views.splash', name='splash'), # 任何以/auth/开头的URL将会匹配,引入openstack_auth.urls url(r'^auth/', include('openstack_auth.urls')), # 任何以/api/开头的URL将会匹配,引入openstack_dashboard.api.rest.urls url(r'^api/', include('openstack_dashboard.api.rest.urls')), # 任何访问URL将会匹配,都引用horizon.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
""" splash()视图函数来处理url(r'^$', 'openstack_dashboard.views.splash', name='splash')。 判断当前访问用户是否登录,如果登录跳转到该用户权限范围内的默认页面,如果没有登录打开登录界面。 """ @django.views.decorators.vary.vary_on_cookie def splash(request): # is_authenticated() 登录用户是否通过验证,也就是通过用户名和密码判断该用户是否存在 if request.user.is_authenticated(): # shortcuts.redirect():返回一个HTTP响应,重定向到适当的URL参数。 # 当前登录用户存在,shortcuts.redirect()重定向到horizon.get_user_home(request.user)返回的地址。 # 假设我们这边访问的用户是"admin"管理员 # horizon.get_user_home(request.user):"/project/" response = shortcuts.redirect(horizon.get_user_home(request.user)) else: # 当前登录用户不存在或者用户没有登录,加载登录表单,跳转到登录页面 # openstack_auth.forms.Login()创建Login登录form表单。 # openstack_auth 身份验证是一个可插拔Django认证后端, # 使用Django的"contrib.auth"框架来调用OpenStack's Keystone的认证API来验证用户。 form = forms.Login(request) # shortcuts.render() 返回一个HttpResponse的内容, # 'auth/login.html':登录表单模板文件 # 'form':传递form表单参数 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下拉选择框,不是必填项 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是一种表单,它的字段是self.fields,这是一个django.utils.datastructures.SortedDict(它呈现在它们被添加的顺序的项目)。 经过结构形式self.fields有keyOrder属性,它是包含在它们应该出现的顺序中的字段名称列表。 你可以将其设置为正确顺序(虽然你需要小心,以确保您不会遗漏项目或增加额外)。 ''' # 设置在form表单控件出现的顺序 self.fields.keyOrder = ['username', 'password', 'region'] if getattr(settings, 'OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT', False): # 读取local_setting.py OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT, True:OpenStack的KEYSTONE多域支持, False :不支持 # Domain:域 不同与 region:区域 # 登录表单上域的文本框。必填项 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: # regions的长度为1即是默认值"Default Region" # 初始参数让你指定在一个未绑定的表单呈现该字段时要使用的初始值。 self.fields['region'].initial = self.fields['region'].choices[0][0] # 设置region 控件为一个隐藏框 self.fields['region'].widget = forms.widgets.HiddenInput() elif len(self.fields['region'].choices) > 1: # 如果长度大于1即horizon配置了多区域,初始化值为本地COOkIES的login_region值 self.fields['region'].initial = self.request.COOKIES.get( 'login_region') @staticmethod def get_region_choices(): # settings.OPENSTACK_KEYSTONE_URL 获取多区域下拉选项值 default_region = (settings.OPENSTACK_KEYSTONE_URL, "Default Region") # settings.AVAILABLE_REGIONS 可用区域的endpoint 和 标题 regions = getattr(settings, 'AVAILABLE_REGIONS', []) if not regions: # regions 设置默认值:“Default Region” 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): """返回一个特定用户的URL 这个方法一个功能是可以定制用户登录界面,默认返回值:get_absolute_url() 另一个功能是可以提供自定义行为通过指定一个URL或一个函数返回一个URL,通过在setting.py文件中定义HORIZON_CONFIG的参数值 例如:setting.py -> HORIZON_CONFIG HORIZON_CONFIG = { 'user_home': 'openstack_dashboard.views.get_user_home', 'ajax_queue_limit': 10, 'auto_fade_alerts': { 'delay': 3000, 'fade_duration': 1500, 'types': ['alert-success', 'alert-info'] }, 'help_url': "http://docs.openstack.org", 'exceptions': {'recoverable': exceptions.RECOVERABLE, 'not_found': exceptions.NOT_FOUND, 'unauthorized': exceptions.UNAUTHORIZED}, 'angular_modules': [], 'js_files': [], } 这可能是有用的如果默认的仪表板可能不是所有用户都可以访问。 当HORIZON_CONFIG 中缺少user_home定义时默认返回settings.LOGIN_REDIRECT_URL值 """ # 调用_conf()函数 返回setting.py 中 HORIZON_CONFIG参数中的user_home值 # admin 用户返回:'openstack_dashboard.views.get_user_home' user_home = self._conf['user_home'] if user_home: # user_home有值 if callable(user_home):# 检查对象user_home是否可调用。 # 因为user_home值是一个字符串不可调用,没有进这个语句块。 return user_home(user) elif isinstance(user_home, basestring):# 判断一个user_home是否为str或者unicode的实例 # 因为user_home是一个str类型的字符串,进这个语句块 # 假设我们的url即user_home值里面有“/” if '/' in user_home:# user_home 值没有"/"没有进这个语句块 return user_home else: # 用"."分割user_home值 mod, func = user_home.rsplit(".", 1) # mod:'openstack_dashboard.views' # func:'get_user_home' # user:<SimpleLazyObject: <User: admin>> # import mod 调用func方法,传入参数user # 执行openstack_dashboard.views.get_user_home() return getattr(import_module(mod), func)(user) # user_home 值如果既不可调用,也不是字符串,认为这是错误的,报一个异常。 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():
# 获取当前用户登录之后跳转的URL地址 def get_user_home(user): dashboard = None if user.is_superuser:# 判断user是否是超级用户 # adminy用户不是超级用户没有执行语句块 try: dashboard = horizon.get_dashboard('admin') except base.NotRegistered: pass if dashboard is None: # dashboard为None 调用horizon.get_default_dashboard()函数 dashboard = horizon.get_default_dashboard() # dashboard: <Dashboard: project> 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): """ 返回“~horizon.Dashboard”默认实例 如果"default_dashboard"在参数"HORIZON_CONFIG"指定了,将返回指定值。 如果没有指定将调用"~horizon.get_dashboards"函数返回"default_dashboard"值 """ # 判断在HORIZON_CONFIG中是否指定了default_dashboard的值 if self.default_dashboard: # self:<Site: horizon> # self.default_dashboard:'project' # 调用self._registered()函数传入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): # self:<Site: horizon> # cls:'project' # inspect.isclass:检查对象cls是否是一个类,issubclass:判断cls类是_registerable_class子类或子孙类 # self._registerable_class:<class 'horizon.base.Dashboard'> -> class:Site _registerable_class = Dashboard if inspect.isclass(cls) and issubclass(cls, self._registerable_class): # 因为cls:'project'是一个字符串条件不成立 # 例如 cls: <class 'openstack_dashboard.dashboards.settings.dashboard.Settings'> 进入代码块 found = self._registry.get(cls, None) if found: return found else: # 循环判断cls是否等于self._registry.values()注册表,如果相等返回这个模块,这个返回<Dashboard: project> # cls="project" -> self._registry.values():[<Dashboard: identity>, <Dashboard: settings>, <Dashboard: project>, <Dashboard: admin>] 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): """返回dashboard的默认URL 默认URL定义为:name="index" 在URLcon:class:`~horizon.Panel`指定的:attr:`~horizon.Dashboard.default_panel`. """ try: """ 获取Dashboard:Default Panel Url self:<Dashboard: project> self.default_panel:'overview' """ 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:def get_absolute_url(self) 返回Panel访问的URL地址:例如:’/project/‘。函数通过调用关系层层返回’/project/‘值,最终返回到openstack_dashboard.views.py:splash(),重定向到/project/地址,显示Project模块内容,Project模块内容加载过程后续博文说明。
以上就是整个Horizon用户访问登录代码分析过程,其中可能有一些理解错误的地方忘谅解。