OpenStack Horizon解析

一、Horizon介绍

​ Horizon是OpenStack的一个子项目,用于提供一个Web前端控制台(称为Dashboard),以此来展示OpenStack的功能。实际上,Horizon并不会为OpenStack添加任何新的功能,它只是使用了OpenStack部分API功能,因此,我们可以扩展Dashboard。

​ Horizon是基于 python Django webframework开发的标准的python wsgi程序。

​ Horizon将页面上所有元素模块化,网页中一些常见元素,表单,表格,标签页,全部封装成Python类,每个组件有自己对应的一小块html模板,当渲染整个页面的时候,Horizon先找到当前页面有多少组件,将各个组件分别进行渲染变成一小段片段,最后拼接成一个完整的html页面,返回浏览器。

​ 总结Horizon的特点:

  • 页面元素模块化
  • 子面板可插拔
  • All in One(从部署上说,Horizon只有它自己一个组件)

二、Horizon项目结构

2.1 整体结构

​ Horizon这套面板的设计分为三层:Dashboard -> PanelGroup -> Panel

dashboard_panel

1
2
3
4
(1) project 普通用户登录后看到的项目面板
(2) admin 管理员登录后可见,左侧的管理员面板
(3) settings 右上角的设置面板,可设置语言,时区,更改密码
(4) identity 不同的角色登陆后,左侧的“身份”面板,可设置项目用户

​ 每一个dashboard都是Django 中的一个 app ,Django中的app可以理解为对业务逻辑化的一种手段,里面可以包含自己独有的url设定,模板,和业务逻辑代码。每个dashboard下定义了一系列的PanelGroup,虚拟机管理对应到界面就是一个PanelGroup(ManageComputer),里面有一系列的子Panel(Overview、Instances、Volumes)。Swift,Heat,Neutron的管理面板各自都是一个PanelGroup,底下有各自的子panel。

2.2项目结构

Horizon的源码中,包含两个主要代码文件夹:horizonopenstack_dashboard

horizon:这个包是在django 基础上写的通用组件,表格(table),标签页(tab),表单(form),面包屑导航(browser),工作流(workflow)。horizon/base.py 中还实现了一套dashboard/panel机制,使得Horizon面板上所有的dashboard都是 可插拔 的,所有的panel都是 动态加载

openstack_dashboard:horizon各个面板的具体实现代码

以下对各个目录做一个介绍,有些目录目前还不知道具体的作用:

horizon项目结构1

horizon项目结构2

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
63
64
horizon
./doc : horizon相关帮助性文档
./horizon : horizon 通用组件库
./browser : 面包屑导航
./conf : horizon配置文件
./contrib : 多语言映射关系
./forms :form表单基类包
./lacale :国际化语言包
./management :manage.py命令的startdash/startpanel命令选项
./static : 静态文件包
./tables : table基类包
./tabs : tab基类包
./templates : 模板文件基类
./templatetags :模板标签基类
./test : 测试包
./utils : 工具包
./workflows : 工作流机制包
./base.py
./context_processors.py : 上下文处理器
./decorators.py :通用装饰器
./exceptions.py : 异常处理
./loaders.py : 加载templates目录的包装饰器
./middleware.py : 中间件
./models.py : 版本信息
./version.py : 版本信息
./site_urls.py : URL相关
./themes.py : 动态加载主题
./views.py :视图
./openstack_dashboard : 各个面板的具体代码实现
./api : 与外部api交互的方法和接口对象
./conf : nova/cinder/glance等API访问权限控制(xxx_policy.josn)
./dashboards : horizon界面展示各个模板实现目录
./admin : 管理员界面
./instances:云主机管理界面
./templates:云主机html界面模板
./forms.py:form表单实现
./panel.py:实现panel注册到dashbord
./tables.py:table实现
./tests.py:测试
./urls.py:url映射
./views.py:url映射的视图
......
./idetity : 项目、用户管理界面
./project :普通用户项目界面
./settings : 设置界面
./django_pyscss_fix:
./enabled :控制导航加载那些模块显示出来
./local : 本地配置文件
./lacale : 本地国家化语言包
./management : 定义安装apache 、horizon等配置文件的模板文件
./static : 静态文件包
./templates : 模板包
./templatetags :模板标签包
./test : 测试包
./usage : 概况页面资源统计实现包
./utils : 工具包
./wsgi : wsgi包
./context_processors.py : 上下文处理器
./exceptions.py : 异常处理
./hooks.py : 钩子函数
./policy.py : 策略
./urls.py :URL模型
./settings.py : 设置文件
./views.py : 视图文件

三、Horizon 注册机制源码解析

3.1 horizon导入

openstack_dashboard/settings.py 导入 horizon 应用

1
2
3
4
5
INSTALLED_APPS = [
……
'horizon',
……
]

python中在导包时,会自动导入它的init.py文件,当我们导入Horizon这个包的时候,init.py文件自动运行,在init.py 文件中再导入其他的包,或者模块。其中在horizon包的init.py文件中

1
2
3
4
# horizon/__init__.py
from horizon.base import Horizon
……
urls = Horizon._lazy_urls # _lazy_urls()是完成整个模块注册的入口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# horizon/base.py
@property
def _lazy_urls(self):
"""
延迟加载URL模式。
这种方法避免试图加载URLconf值之前设置模块已经加载的问题。
"""
def url_patterns():
"""
request 请求进来的时候,会去Openstack.urls.py 进行配置,include('openstack_auth.urls')这个 时候会执行url_patterns()
"""
return self._urls()[0]
# 其中LazyURLPattern(url_patterns)没有继续执行下去,LazyURLPattern()继承自Djano # SimpleLazyObject理解成将一个方法转换成延迟执行的方法,只有真正调用的时候才会执行,显然这里没 # 有调用,这里只是一个加载,url_patterns作为一个方法引用当作参数。
return LazyURLPattern(url_patterns), self.namespace, self.slug
  • 注 :horizon采用单例的设计模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class HorizonSite(Site):
    """A singleton implementation of Site such that all dealings with horizon
    get the same instance no matter what. There can be only one.
    """
    _instance = None

    def __new__(cls, *args, **kwargs):
    if not cls._instance:
    cls._instance = super(Site, cls).__new__(cls, *args, **kwargs)
    return cls._instance

    # The one true Horizon
    Horizon = HorizonSite()
3.2 request请求

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# openstack_dashboard/urls.py
urlpatterns = patterns(
'',
# 匹配网站根目录的URL,映射到openstack_dashboard.views.splash视图。
url(r'^$', 'openstack_dashboard.views.splash', name='splash'),
# 任何以/api/开头的URL将会匹配,引入openstack_dashboard.api.rest.urls
url(r'^api/', include('openstack_dashboard.api.rest.urls')),
# 任何访问URL将会匹配,都引用horizon.urls
url(r'', include(horizon.urls)), # urls = Horizon._lazy_urls
)

for u in getattr(settings, 'AUTHENTICATION_URLS', ['openstack_auth.urls']):
urlpatterns += patterns(
'',
# 任何以/auth/开头的URL将会匹配,引入openstack_auth.urls
url(r'^auth/', include(u))
)

request –>> url(r’’, include(horizon.urls)) : horizon.urls 对应的是 horizon.base._lazy_urls() ,

​ –>> 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
# horizon.base.Site.urls()
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
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参数值
([,
\S+?)/$>,
,
(None:None) ^i18n/>,
(settings:settings) ^settings/>,
(identity:identity) ^identity/>,
(project:project) ^project/>,
(admin:admin) ^admin/>,
(settings:settings) ^settings/>,
(identity:identity) ^identity/>,
(project:project) ^project/>,
(admin:admin) ^admin/>],
'horizon',
'horizon')
"""
return urlpatterns, self.namespace, self.slug
  • 标记(1)从“settings.INSTALLED_APPS发现模块,包含dashboard.py、panel.py的模块注册,添加到注册表中self._registry,没有的抛出异常。
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
def _autodiscover(self):
"""
从“settings.INSTALLED_APPS发现模块注册的,确保适当的模块引入并注册到horizon中
"""
# 你必须设置一个“registerable_class”属性以使用自动发现。
# _registerable_class = Panel _registerable_class = Dashboard
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
'''
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
  • 标记(2)发现每个Dashboards里的Panels,从注册表self._registry 取出注册dashboard,注册每个dashboard中的panel
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
# horizon.base.Dashboard._autodiscover():
def _autodiscover(self):
"""
从当前的dashboard模块中发现panel并注册
"""
# self._autodiscover_complete:True 自动完成发现否时完成的一个标记
if getattr(self, "_autodiscover_complete", False):
return
panels_to_discover = []
panel_groups = []

# basestring:basestring是str和unicode的父类,也是抽象类,因此不能被调用和实例化,
# 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类。
if not isinstance(panel_set, collections.Iterable) and \
issubclass(panel_set, PanelGroup):
panel_group = panel_set(self)

# 检查嵌套的元组,并将其转换成PanelGroups
elif not isinstance(panel_set, 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': }
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

四、Horizon 用户登录流程

​ 用户请求过来,首先到openstack_dashboard/urls.py,查找url对应的视图函数。

1
2
3
4
5
urlpatterns = patterns(
……
url(r'^$', 'openstack_dashboard.views.splash', name='splash'),
……
)

进入openstack.views.splash函数。如果用户已经验证过,则直接进入主页面,否则转到登录验证模块;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@django.views.decorators.vary.vary_on_cookie
def splash(request):
# 调用Django的权限认证机制,根据用户名和密码判断用户是否存在
if not request.user.is_authenticated():
raise exceptions.NotAuthenticated()

# 认证通过,重定向到该用户权限对应的界面
# horizon.get_user_home(request.user) 该函数返回url地址
response = shortcuts.redirect(horizon.get_user_home(request.user))

if 'logout_reason' in request.COOKIES:
response.delete_cookie('logout_reason')
# Display Message of the Day message from the message files
# located in MESSAGES_PATH
if MESSAGES_PATH:
notifications.process_message_notification(request, MESSAGES_PATH)
return response

如未登录,则需要转到Horizon项目的验证插件openstack_auth,然后从这个插件中加载urls.py;

1
2
3
4
urlpatterns = [
url(r"^login/$", views.login, name='login'),
……
]
1
2
3
4
5
6
7
8
9
10
11
12
13
@sensitive_post_parameters()
@csrf_protect
@never_cache
def login(request, template_name=None, extra_context=None, **kwargs):
…………
# 内部继续调用了django的认证 views.login()
res = django_auth_views.login(request,
template_name=template_name,
authentication_form=form,
extra_context=extra_context,
**kwargs)
…………
return res

django的认证 views.login函数如下:

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
@sensitive_post_parameters()
@csrf_protect
@never_cache
def login(request, template_name='registration/login.html',
redirect_field_name=REDIRECT_FIELD_NAME,
authentication_form=AuthenticationForm,
current_app=None, extra_context=None):
"""
Displays the login form and handles the login action.
"""
redirect_to = request.POST.get(redirect_field_name,
request.GET.get(redirect_field_name, ''))

if request.method == "POST":
form = authentication_form(request, data=request.POST)

# is_valid() 主要通过keystone验证用户是否有效
if form.is_valid():

# Ensure the user-originating redirection url is safe.
if not is_safe_url(url=redirect_to, host=request.get_host()):
redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)

# Okay, security check complete. Log the user in.
auth_login(request, form.get_user())

return HttpResponseRedirect(redirect_to)
else:
form = authentication_form(request)

…………

return TemplateResponse(request, template_name, context)

openstack_auth form.py 继承 django_auth_forms.AuthticationForm 类,也就意味着openstack_auth的form.py会与django中的form类中的验证函数相结合,其中django_auth_forms.AuthticationForm类有一个钩子函数clean()函数,它将抛出所有的验证异常,而此钩子函数的具体实现则在子类openstack_auth中form.py中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def clean(self):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')

if username and password:
self.user_cache = authenticate(username=username,
password=password)
if self.user_cache is None:
raise forms.ValidationError(
self.error_messages['invalid_login'],
code='invalid_login',
params={'username': self.username_field.verbose_name},
)
else:
self.confirm_login_allowed(self.user_cache)

return self.cleaned_data

keystone 验证用户名和密码的详细过程,其主要流程大概是先加载openstack_auth中的 backend.py 中的认证函数authenticate()来完成keystone的认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def authenticate(self, auth_url=None, **kwargs):
"""Authenticates a user via the Keystone Identity API."""
LOG.debug('Beginning user authentication')

if not auth_url:
auth_url = settings.OPENSTACK_KEYSTONE_URL

auth_url, url_fixed = utils.fix_auth_url_version_prefix(auth_url)
if url_fixed:
LOG.warning("The OPENSTACK_KEYSTONE_URL setting points to a v2.0 "
"Keystone endpoint, but v3 is specified as the API "
"version to use by Horizon. Using v3 endpoint for "
"authentication.")

for plugin in self.auth_plugins:
unscoped_auth = plugin.get_plugin(auth_url=auth_url, **kwargs)

if unscoped_auth:
break
…………
------ 本文结束------
坚持原创技术分享,您的支持将鼓励我继续创作!
0%