首页 > 代码库 > horizon源码分析(一)

horizon源码分析(一)

 

 

一、源码分析环境:

本来应该搭建horizon的development环境的,这样方便debug,但是由于各种报错,本人没有搭建成功,这也导致有很多源码疑问没有解决,后续可以继续补充这一部分。

官方搭建方法参考网址:http://docs.openstack.org/developer/horizon/quickstart.html

openstack版本:stable-havana

 

 

二、源码分析过程中使用的软件:

SourceInsight:这个当然是源码分析第一利器,可以进行全局关键字查找,非常方便。但是我目前发现其对python支持不是特别好,不过也可以凑合着用。

OneNote:虽然这个一个笔记软件,但是由于其使用起来很像白板,所以可以像在纸上绘图写字一样在里面分析函数的调用关系。

snagit:一个截图软件,可以对各个地方进行截图,相当方便,截图后粘贴到OneNote里面分析。

notepad++:用于在源码分析过程中记录笔记,不解释

visio:用于绘制各种图表

 

 

 

 

三、简要说明

首先,我觉得源码分析实在是一件体力活而非技术活,即便自己看不懂也可能是源码作者自己写得晦涩,况且现在开源社区越来越成熟,开源软件本身为了推行使用,也会不断增加说明文档。所以,看源码还是为了学习和模仿,说白了就是等某时候需要的时候脑子里可能闪现曾经见过别人这么做或者可以这么做。源码分析的结果还是要看重实现原理和使用的技巧,具体细节实在没有太多必要纠结,所以以下列出的源码部分基本不是完整的,只摘取了部分!

这次分析horizon并非纯粹为了看源码,主要是分析一个前端页面请求没有响应的bug,带着这个问题去梳理整个源码分析流程!

本人初次接触python、Django以及horizon,还有很多东西不是很熟悉,还希望大家一起多交流,如有错误,欢迎批评指正!

 

 

 

 

 

四、horizon源码分析思路

还是说一下我分析的思路,首先horizon本身基于Django实现,简单地说就是网站的架构,主要分析前端请求和后端处理。所以第一步就是找到页面请求的具体内容,这个我使用的是chrome的开发者工具;第二步是找到前端请求与后端处理的绑定,具体为此次动作由哪个函数来处理等等;第三步,可以料想horizon必然会调用openstack其他组件的API接口,这里只分析后端处理流程,止步于API调用,暂且不详细分析。

 

 

 

 

五、horizon前端请求

主要用浏览器chrome的开发者工具分析页面发出的具体请求,此处是分析前端的一个button按钮执行情况。

 

具体的button元素:

 

请求链接地址:

 

其他相关元素:

 

总结如下:

地址:/dashboard/admin/instances/【1】

方式:POST

参数:

instances_filter_q:

action:instances__soft_reboot__89a8849b-a3cd-4ce0-9158-c3dd69e8508e

 

说明:【1】请求地址中的/dashboard/应该和Apache服务器配置有关,这个可以后续看一下horizon的安装配置过程

 

 

 

六、URL与后端的绑定

根据Django的框架结构,使用URLconf文件(https://docs.djangoproject.com/en/1.4/topics/http/urls/)进行链接请求和后端处理(view)的绑定,使用view进行后端处理,使用template进行页面渲染。

 

1、目录结构:

horizon-------------组件,提供部分功能

    |---__init__.py-----------控制着horizon包导入行为

    |---base.py-----------horizon提供的Site类、Dashboard类、Panel类,负责整个基本架构

    |---site_urls.py

    

openstack_dashboard----------------网站project根目录

    |

    |---settings.py------------网站基本设置【1】

    |---urls.py----------------网站基本URL设置

    |---views.py---------------网站基本view

    |---templates--------------网站基本template

    |

    |---dashboards

        |---admin ---------------dashboard【2】

            |---instances --------------panel【3】

                |---panel.py----------------------负责往dashboard中注册panel

                |---urls.py

                |---views.py

            ...

            |---dashboard.py --------------负责往Horizon中注册dashboard

            |---models.py

            

        ...

        

说明:这里只列出了和本次分析有关的部分目录和文件。【1】还有一个openstack_dashboard/local/local_settings.py,貌似那个是本地设置,暂且还不清楚settings.py和其是如何结合的。

【2】【3】这里的dashboard和panel是horizon自定义的组件,对应页面上的相应部分。具体如下图所示:

 

2、URL绑定分析:

openstack_dashboard/settings.py

ROOT_URLCONF = ‘openstack_dashboard.urls‘

 

openstack_dashboard/urls.py

 

使用了一个小trick,在导入horizon这个package时

horizon/__init__.py

 

【小杰鱼说】在package的__init__.py中控制包的导入行为,这样可以使包的使用更加简洁方便

 

horizon/base.py

基本的类关系:

 

part1:

 

part2:

 

part3:

Site类:

 

综上所述,最终的include()导入的是Site类的_lazy_urls

 

 

3、说明一下Site、Dashboard、Panel三者之间的关系:

【小杰鱼说】这里Horizon采用了注册机制,即以一个对象为根对象,将其他组件注册到根对象的属性中,这种机制使得软件的可扩展性更强,并且条理清晰。貌似像一种设计模式来着。

 

Horizon是一个Site类对象,往其中注册Dashboard类时会构建一个Dashboard对象注册到其属性_registry字典中,Panel类往Dashboard类中注册,注册时会构建Panel对象注册到Dashboard对象的_registry字典里。具体情况如下:

 

一个dashboard注册过程

举例为admin:

openstack_dashboard/dashboards/admin/dashboard.py

import horizon

 

class SystemPanels(horizon.PanelGroup):

slug = "admin"

name = _("System Panel")

panels = (‘overview‘, ‘metering‘, ‘hypervisors‘, ‘instances‘, ‘volumes‘,

‘flavors‘, ‘images‘, ‘networks‘, ‘routers‘, ‘defaults‘, ‘info‘)

 

 

class IdentityPanels(horizon.PanelGroup):

slug = "identity"

name = _("Identity Panel")

panels = (‘domains‘, ‘projects‘, ‘users‘, ‘groups‘, ‘roles‘)

 

 

class Admin(horizon.Dashboard):

name = _("Admin")

slug = "admin"

"""panels可以用来发现与该Dashboard相关的所有Panel,以便在以后往Dashboard中注册这些Panel"""

panels = (SystemPanels, IdentityPanels)

default_panel = ‘overview‘

permissions = (‘openstack.roles.admin‘,)

 

horizon.register(Admin)

 

horizon/base.py

Site类:

def register(self, dashboard):

"""Registers a :class:`~horizon.Dashboard` with Horizon."""

return self._register(dashboard)

 

Registry类:

def _register(self, cls):

"""Registers the given class.

 

If the specified class is already registered then it is ignored.

"""

if not inspect.isclass(cls):

raise ValueError(‘Only classes may be registered.‘)

elif not issubclass(cls, self._registerable_class):

raise ValueError(‘Only %s classes or subclasses may be registered.‘

% self._registerable_class.__name__)

 

if cls not in self._registry:

cls._registered_with = self

self._registry[cls] = cls()

 

return self._registry[cls]

 

在horizon/base.py中的Horizon对象的_registy映射中添加Dashboard类à类实例的映射

 

一个panel的注册过程

举例为admin中的instancs:

openstack_dashboard/dashboards/admin/instances/panel.py

import horizon

"""将admin这个Dashboard的dashboard.py导入"""

from openstack_dashboard.dashboards.admin import dashboard

 

 

class Aggregates(horizon.Panel):

name = _("Host Aggregates")

slug = ‘aggregates‘

permissions = (‘openstack.services.compute‘,)

 

"""在Dashboard为Admin中进行注册"""

dashboard.Admin.register(Aggregates)

 

horizon/base.py

Dashboard类:

@classmethod

def register(cls, panel):

"""Registers a :class:`~horizon.Panel` with this dashboard."""

"""检查cls是否已经注册到Horizon对象中,并且在cls中注册panel"""

panel_class = Horizon.register_panel(cls, panel)

# Support template loading from panel template directories.

panel_mod = import_module(panel.__module__)

panel_dir = os.path.dirname(panel_mod.__file__)

template_dir = os.path.join(panel_dir, "templates")

if os.path.exists(template_dir):

key = os.path.join(cls.slug, panel.slug)

loaders.panel_template_dirs[key] = template_dir

return panel_class

 

panel的注册会先从Horizon对象中找出对应的已经注册的Dashboard对象,然后在该Dashboard对象里面注册Panel,注册过程和Dashboard的注册过程类似

 

 

继续分析...

 

 

horizon/base.py

Site类:

 

Site类:

def _urls(self):

 

        """实际调用HorizonComponent._get_default_urlpatterns,获取site_urls.py中的urlpatterns"""

        urlpatterns = self._get_default_urlpatterns()【1】

        

        """实际调用Site._autodiscover ,导入openstack_dashboard/settings.py中的HORIZON_CONFIG, INSTALLED_APPS配置的模块中的dashboard.py和panel.py,此处会进行Dashboard的注册"""[猜测]

self._autodiscover()【2】

 

"""发现每个Dashboard中的Panel,导入每个Panel的panel.py,从而注册每个Panel"""

for dash in self._registry.values():

dash._autodiscover()【3】

 

        ...

 

# Compile the dynamic urlconf.

        """dash为Dashboard类"""

for dash in self._registry.values():

urlpatterns += patterns(‘‘,

url(r‘^%s/‘ % dash.slug, include(dash._decorated_urls)))

 

# Return the three arguments to django.conf.urls.include

return urlpatterns, self.namespace, self.slug

 

 

 

Dashboard类:

@property

def _decorated_urls(self):

urlpatterns = self._get_default_urlpatterns()【1】

 

default_panel = None

 

# Add in each panel‘s views except for the default view.

"""panel为Panel类"""

for panel in self._registry.values():

if panel.slug == self.default_panel:

default_panel = panel

continue

url_slug = panel.slug.replace(‘.‘, ‘/‘)

urlpatterns += patterns(‘‘,

url(r‘^%s/‘ % url_slug, include(panel._decorated_urls)))

 

...

 

# Return the three arguments to django.conf.urls.include

return urlpatterns, self.slug, self.slug

 

 

 

Panel类:

@property

def _decorated_urls(self):

 

        """即查找panel的urls.py文件"""

urlpatterns = self._get_default_urlpatterns()【1】

 

# Apply access controls to all views in the patterns

permissions = getattr(self, ‘permissions‘, [])

_decorate_urlconf(urlpatterns, require_perms, permissions)

_decorate_urlconf(urlpatterns, _current_component, panel=self)

 

# Return the three arguments to django.conf.urls.include

return urlpatterns, self.slug, self.slug

 

 

4、结论:

对于请求:

地址:/dashboard/admin/instances/

方式:POST

参数:

instances_filter_q:

action:instances__soft_reboot__89a8849b-a3cd-4ce0-9158-c3dd69e8508e

 

URL绑定为:

openstack_dashboard/dashboards/admin/instances/urls.py

 

 

5、horizon/base.py中API注解

【1】HorizonComponent. _get_default_urlpatterns

获取对象的urls属性指定的或对象所处的package下面的urls.py中的urlpatterns

 

【2】Site._autodiscover

def _autodiscover(self):

"""Discovers modules to register from ``settings.INSTALLED_APPS``.

 

This makes sure that the appropriate modules get imported to register

themselves with Horizon.

"""

if not getattr(self, ‘_registerable_class‘, None):

raise ImproperlyConfigured(‘You must set a ‘

‘"_registerable_class" property ‘

‘in order to use autodiscovery.‘)

 

# Discover both dashboards and panels, in that order

for mod_name in (‘dashboard‘, ‘panel‘):

"""settings.INSTALLED_APPS感觉应该和openstack_dashboard/settings.py中的HORIZON_CONFIG, INSTALLED_APPS有关"""

for app in settings.INSTALLED_APPS:

mod = import_module(app)

try:

before_import_registry = copy.copy(self._registry)

import_module(‘%s.%s‘ % (app, mod_name))

except Exception:

self._registry = before_import_registry

if module_has_submodule(mod, mod_name):

raise

 

导入settings.INSTALLED_APPS中的各个模块中的dashboard.py或panel.py,举例说就是openstack_dashboard/dashboards中各个dashboard的dashboard.py,在导入dashboard.py或panel.py时会执行相应的注册行为

 

 

【3】Dashboard._autodiscover

导入当前Dashboard中的各个Panel的panel.py,其中会进行Panel的注册

 

horizon源码分析(一)