Horizon 源码阅读(二)—— Horizon 模块注册机制

一、写在前面

这篇文章主要介绍一下OpenStack Horizon — juno项目模块注册机制,本文将通过对Horizon源码解析了解各个模块注册加载机制。全文穿插了Horizon组建的代码块以及个人理解注释,由于能力和时间有限,错误之处在所难免,欢迎指正!

       如果转载,请保留作者信息。
       邮箱地址:jpzhang.ht@gmail.com


二、Horizon模块注册机制

       Horizon/base.py中实现了一套dashboard/panel注册、动态加载机制,使得Horizon面板上所有的dashboard都是”可插拔”的,所有的panel都是”动态加载”的,在以下的内容里将通过源码解析具体分析horizon/base.py文件,揭开这神秘的面纱。

       流程图: 

       

       整个Horizon模块的加载流程如上图所示,horizon项目在引入的时候自动通过horizon.__init__()自动Load类属性方法def_lazy_urls(self),加载LazyURLPattern(url_patterns)其中url_patterns是一个嵌套方法,LazyURLPattern继承自Django SimpleLazyObject类,个人理解这是把_lazy_urls()方法转换成一个懒惰方法,这里只加载不执行,等正真用到的时候在执行。

       当用户通过Request请求访问的时候,根据Django Urls映射机制,会去openstack_dashboard.urls.py匹配,urls.py文件中url(r’', include(horizon.urls)),触发执行_lazy_urls()方法。_lazy_urls()方法返回def_urls(self)处理结果的第一个参数urlpatterns,整个Horizon模块的注册Urls编译都是在这个方法内完成,通过_autodiscover()方法,从“settings.INSTALLED_APPS发现模块,包含dashboard.py文件的模块进行注册,并添加到self._registry注册表中,然后通过循环遍历注册表,调用每个注册dashboard的_autodiscover()方法,注册每个dashboard下面的panel,完成整个horizon模块的注册,最终返回一个urlpatterns值,urls匹配调用相应的views模块。

       

 注意点:

          1、settings.INSTALLED_APPS在这里拉出来讲解一下,settings.INSTALLED_APPS是一个字符串tuple ,内容是项目中引入的应用,在Horizon项目中有两部分组成:

第一部分:settings.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
             
             
INSTALLED_APPS = [ 'openstack_dashboard' ,
'django.contrib.contenttypes' ,
'django.contrib.auth' ,
'django.contrib.sessions' ,
'django.contrib.messages' ,
'django.contrib.staticfiles' ,
'django.contrib.humanize' ,
'django_pyscss' ,
'openstack_dashboard.django_pyscss_fix' ,
'compressor' ,
'horizon' ,
'openstack_auth' ,]
 来自CODE的代码片
horizon(2-1).py

第二部分:settings.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
             
             
# Load the pluggable dashboard settings
import openstack_dashboard.enabled
import openstack_dashboard.local.enabled
from openstack_dashboard.utils import settings
INSTALLED_APPS = list ( INSTALLED_APPS ) # Make sure it's mutable
settings . update_dashboards ([
openstack_dashboard . enabled ,
openstack_dashboard . local . enabled ,
], HORIZON_CONFIG , INSTALLED_APPS )
 来自CODE的代码片
horizon(2-2).py

openstack_dashboard.enabled/openstack_dashboard.local.enabled:配置启用和显示Daishboard,用来配置Horizon左侧导航显示的dashboard。自己开发一个dashboard需要在openstack_dashboard/enabled 添加一个类似 _50_mydashboard.py文件,这样才会被注入到settings.INSTALLED_APPS配置项中。整个settings.INSTALLED_APPS中引入的APP由这两部分组拼在一起。

2、每个dashboard模块下都有一个dasbboard.py文件里面定义了属于当前dashboard的PanelGroup,和各个PanelGroup下的Panel,在Horizon模块被导入的时候会去依次遍历Dasboard→PanelGroup→Panel,所有的dashboard注册到Horizon命名空间下,各个panel注册到自己的dashboard命名空间下。

三、Horizon模块注册源码解析

       接下去刚通过分析Juno 中Horizon关于这部分的原码来讲解。

       细粒度的数据流图: 

       

       上图是关于Horizon中关于模块注册的数据流图。


1)horizon导入

      settings.py-> 导入horizon

      INSTALLED_APPS = [
                                       ‘horizon',

                                        ……
       ]

Python中在导入一个包时,实际上导入了它的__init__.py文件,当我们导入Horizon这个包的时候,__init__.py文件自动运行,在__init__.py 文件中再导入其他的包,或者模块。其中在horizon包的__init__.py文件中:

    from horizon.baseimport Horizon  # noqa

    ……
    urls = Horizon._lazy_urls

    ……


导入了Horizon.base._lazy_urls方法,_lazy_urls()是完成整个模块注册的入口,Horizon.base._lazy_urls方法增加了@property, @property 可以将python定义的函数“当做”属性访问。__init__.py将Horizon.base._lazy_urls导入的时候自动执行该方法体。


注:horizon提供的Site类、Dashboard类、Panel类,负责整个基本架构,实现单例的设计模式,全局只有一个只有一个horizon对象。 对于这个文件代码架构的分析放到以后。

       
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
           
           
@property
def _lazy_urls ( self ):
"""
延迟加载URL模式。
这种方法避免试图加载URLconf值之前设置模块已经加载的问题。
"""
def url_patterns ():
return self . _urls ()[ 0 ]
# LazyURLPattern(url_patterns) 猜测这是一个懒惰方法,load的时候把方法传入,当需要使用的使用才执行方法。
# request 请求进来的时候,回去Openstack.urls.py 进行配置,include('openstack_auth.urls')这个时候会执行url_patterns()
# self.namespace = "horizon"
# self.slug = "horizon"
return LazyURLPattern ( url_patterns ), self . namespace , self . slug
 来自CODE的代码片
horizon(2-4).py

其中LazyURLPattern(url_patterns)没有继续执行下去,LazyURLPattern()继承自Djano SimpleLazyObject理解成将一个方法转换成延迟执行的方法,只有正真调用的时候才会正真执行,显然这里没有正真调用,这里只是一个加载,url_patterns作为一个方法引用当作参数。

     

2)ruquest请求

      当用户通过浏览器发起request请求,根据Django的框架结构,使用URLconf进行连接请求和后端处理(view)的绑定,使用view进行后端处理,使用template进行页面渲染。

      

     
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
           
           
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 )),
)
 来自CODE的代码片
horizon(2-5).py

       openstack_dashboard.urls.py:

       url(r'', include(horizon.urls)): horizon.urls对应的是horizon.baise._lazy_urls(),执行之前导入horizon包加载_lazy_urls()方法中的

            def url_patterns():
                 
return self._urls()[0]


接着调用 horizon.base.Site._urls(),完成所有Dashboard和Panel的注册变编译URLconf:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
           
           
def _urls ( self ):
"""
从注册的Dashboards构造Horizon的Urlconf
"""
# 获取horizon.site_urls urlpatterns 值
urlpatterns = self . _get_default_urlpatterns ()
# 从“settings.INSTALLED_APPS发现模块,包含dashboard.py、panel.py的模块注册,添加到注册表中self._registry,没有的抛出异常。
self . _autodiscover () - -------------------标注( 1
# 发现每个Dashboards里的Panels
# 从注册表self._registry 取出注册dashboard,注册每个dashboard中的panel
# self._registry:{<class 'openstack_dashboard.dashboards.identity.dashboard.Identity'>: <Dashboard: identity>,
# <class 'openstack_dashboard.dashboards.project.dashboard.Project'>: <Dashboard: project>,
# <class 'openstack_dashboard.dashboards.admin.dashboard.Admin'>: <Dashboard: admin>,
# <class 'openstack_dashboard.dashboards.settings.dashboard.Settings'>: <Dashboard: settings>}
#self._registry.values():[<Dashboard: identity>, <Dashboard: project>, <Dashboard: admin>, <Dashboard: settings>]
for dash in self . _registry . values ():
dash . _autodiscover () -----------------标注( 2
# 加载基于插件的面板配置
self . _load_panel_customization ()
# 允许覆盖模块
if self . _conf . get ( "customization_module" , None ):
customization_module = self . _conf [ "customization_module" ]
bits = customization_module . split ( '.' )
mod_name = bits . pop ()
package = '.' . join ( bits )
mod = import_module ( package )
try :
before_import_registry = copy . copy ( self . _registry )
import_module ( ' %s . %s ' % ( package , mod_name ))
except Exception :
self . _registry = before_import_registry
if module_has_submodule ( mod , mod_name ):
raise
# 编译动态URL配置。
for dash in self . _registry . values ():
urlpatterns += patterns ( '' ,
url ( r'^ %s /' % dash . slug ,
include ( dash . _decorated_urls )))
# 返回三个参数,django.conf.urls.include
""
返回的 urlpattern 参数值
([ < RegexURLPattern user_home ^ home / $ > ,
< RegexURLPattern jsi18n ^ i18n / js / ( ? P < packages >\ S + ? ) / $ > ,
< RegexURLPattern set_language ^ i18n / setlang / $ > ,
< RegexURLResolver < module 'django.conf.urls.i18n' from '/usr/lib/python2.7/dist-packages/django/conf/urls/i18n.pyc' > ( None : None ) ^ i18n /> ,
< RegexURLResolver < RegexURLResolver list > ( settings : settings ) ^ settings /> ,
< RegexURLResolver < RegexURLResolver list > ( identity : identity ) ^ identity /> ,
< RegexURLResolver < RegexURLResolver list > ( project : project ) ^ project /> ,
< RegexURLResolver < RegexURLResolver list > ( admin : admin ) ^ admin /> ,
< RegexURLResolver < RegexURLResolver list > ( settings : settings ) ^ settings /> ,
< RegexURLResolver < RegexURLResolver list > ( identity : identity ) ^ identity /> ,
< RegexURLResolver < RegexURLResolver list > ( project : project ) ^ project /> ,
< RegexURLResolver < RegexURLResolver list > ( admin : admin ) ^ admin /> ],
'horizon' ,
'horizon' )
"""
return urlpatterns, self.namespace, self.slug
 来自CODE的代码片
horizon(2-6).py

标记(1)从“settings.INSTALLED_APPS发现模块,包含dashboard.py、panel.py的模块注册,添加到注册表中self._registry,没有的抛出异常。

                horizon.Site._autodiscover():

               
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
           
           
def _autodiscover ( self ):
"""
从“settings.INSTALLED_APPS发现模块注册的
确保适当的模块引入并注册到horizon中
self = <Site: horizon>
"""
# 判断self对象中是否存在_registerable_class 如果没有抛出异常,
# 你必须设置一个“registerable_class”属性以使用自动发现。
# self._registerable_class = <class 'horizon.base.Dashboard'> 这在Class Site中已经设定了。
if not getattr ( self , '_registerable_class' , None ):
raise ImproperlyConfigured ( 'You must set a '
'"_registerable_class" property '
'in order to use autodiscovery.' )
'''
循化settings.INSTALLED_APPS,导入每个APP中的dashboard、panel模块,即每个模块下面的dashboard.py 和 panel.py
settings.INSTALLED_APPS => ['openstack_dashboard.dashboards.project',
'openstack_dashboard.dashboards.admin',
'openstack_dashboard.dashboards.identity',
'openstack_dashboard.dashboards.settings',
'openstack_dashboard',
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
'django_pyscss',
'openstack_dashboard.django_pyscss_fix',
'compressor',
'horizon',
'openstack_auth']
'''
for mod_name in ( 'dashboard' , 'panel' ):
for app in settings . INSTALLED_APPS :
mod = import_module ( app )
try :
'''
注册表 self._registry = {}
定义:class Registry(object):
def __init__(self):
self._registry = {}
......
copy.copy():copy.copy 浅拷贝 只拷贝父对象,不会拷贝对象的内部的子对象。
'''
before_import_registry = copy . copy ( self . _registry )
"""
import_module() 执行这个模块
例如:app:'openstack_dashboard.dashboards.project'
mod_name:'dashboard'
import_module('%s.%s' % (app, mod_name)) => 执行openstack_dashboard.dashboards.project.dashboard.py => horizon.register(Project)
"""
import_module ( ' %s . %s ' % ( app , mod_name ))
except Exception :
# 如果APP中没有dashboard抛出异常
self . _registry = before_import_registry
if module_has_submodule ( mod , mod_name ):
raise
 来自CODE的代码片
horizon(2-7).py

标记(2)发现每个Dashboards里的Panels,从注册表self._registry 取出注册dashboard,注册每个dashboard中的panel

                horizon.Dashboard_autodiscover():

               
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
           
           
def _autodiscover ( self ):
"""
从当前的dashboard模块中发现panel并注册
"""
# self._autodiscover_complete:True 自动完成发现否时完成的一个标记
if getattr ( self , "_autodiscover_complete" , False ):
return
# self._autodiscover_complete:False
panels_to_discover = []
panel_groups = []
# all(iterable):如果iterable的所有元素不为0、''、False或者iterable为空,all(iterable)返回True,否则返回False;
# basestring:basestring是str和unicode的超类(父类),也是抽象类,因此不能被调用和实例化,
# 但可以被用来判断一个对象是否为str或者unicode的实例,isinstance(obj, basestring)等价于isinstance(obj, (str, unicode));
# isinstance: 判断一个对象是否是一个已知的类型
#
#例如: self:<Dashboard: identity>
# self.panels:('domains', 'projects', 'users', 'groups', 'roles')
if all ([ isinstance ( i , basestring ) for i in self . panels ]):
self . panels = [ self . panels ]
# 现在迭代设置panel
# self.panels: [('domains', 'projects', 'users', 'groups', 'roles')]
for panel_set in self . panels :
# 实例化PanelGroup类。
# panel_set:('domains', 'projects', 'users', 'groups', 'roles')
if not isinstance ( panel_set , collections . Iterable ) and \
issubclass ( panel_set , PanelGroup ): # 返回错误,panel_set不是一个class TypeError('issubclass() arg 1 must be a class',)
panel_group = panel_set ( self )
# 检查嵌套的元组,并将其转换成PanelGroups
elif not isinstance ( panel_set , PanelGroup ):
# 返回一个<class 'horizon.base.PanelGroup'>对象
panel_group = PanelGroup ( self , panels = panel_set )
# 存放返回结果
# 例如:panel_group.panels:['domains', 'projects', 'users', 'groups', 'roles']
panels_to_discover . extend ( panel_group . panels )
panel_groups . append (( panel_group . slug , panel_group ))
# self._panel_groups:{'default': <PanelGroup: default>}
self . _panel_groups = SortedDict ( panel_groups )
# 加载panel_groups中的每一个panel
# package:'openstack_dashboard.dashboards.settings'
package = '.' . join ( self . __module__ . split ( '.' )[: - 1 ])
mod = import_module ( package )
for panel in panels_to_discover :
try :
before_import_registry = copy . copy ( self . _registry )
import_module ( '. %s .panel' % panel , package )
except Exception :
self . _registry = before_import_registry
if module_has_submodule ( mod , panel ):
raise
# 标记自动注册Panel已经完成
self . _autodiscover_complete = True
 来自CODE的代码片
horizon(2-8).py
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值