使用Horizon构建Dashboard

使用Horizon构建Dashboard

一、写在前面

继上篇博文,在这篇博文中举例说明扩展一个dashboard 以及 panel。类似这样的博文在网上较多,这里仅仅是回忆下,好久没有跟进OpenStack Horizon版本的代码,基于目前OpenStack rocky版本。

邮箱地址:jpzhang.ht@gmail.com
个人博客:https://jianpengzhang.github.io/
CSDN博客:http://blog.csdn.net/u011521019
Horizon 原文阅读地址:
https://docs.openstack.org/horizon/latest/contributor/tutorials/dashboard.html



# 二、新建Dashboard & Panel
按照惯例,这里简单介绍下Horizon的目录结构,至于如何构建一个Horizon的开发环境这里不在详述,这里是基于devstack部署的all in one 简单环境。

#### Horizon设计和架构的核心价值:

核心支持:对所有核心OpenStack项目提供开箱即用的支持; 可扩展性:任何开发者都能增加组件;
易于管理:架构和代码易于管理,浏览方便; 视图一致:各组件的界面和交互模式保持一致;
可兼容性:API向后兼容; 易于使用:界面用户友好;

#### Horizon 结构:
Horizon提供一个模块化的基于Web的图形界面,采用了Django框架。Horizon目录结构主要分为两个:Horizon、opendtack_dashboard,Horizon模块的实现充分利用了很多Django框架提供的一些高级特性。如果把horizon目录中的各种实现当作积木,那么搭好的小房屋就放在openstack_dashboard的目录下。目录源码结构如下:
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
.
├── horizon # horizon通用组件库
│   ├── base.py # 定义了horizon模块中从HorizonSite到Panel的各种组件的类库,并且用类HorizonSite为整个项目实例化了一个horizon对象
│   ├── browsers # 浏览器基类
│   ├── conf # 配置文件
│   ├── context_processors.py # 自动设置相应上下文变量来解析模板的上下文处理器,他把HORIZON_CONFIG字典自动加到了Context上下文变量中传给模板
│   ├── contrib # 多语言映射关系
│   ├── decorators.py # 用于方便取得网页请求的当前组件和权限认证
│   ├── exceptions.py # 异常处理
│   ├── forms # form表单基类包
│   ├── hacking
│   ├── __init__.py
│   ├── karma.conf.js
│   ├── loaders.py # 帮助horizon模块使用自定义的方式加载模板而非使用Django自带的模板加载器
│   ├── locale # 国际化语言包
│   ├── management # manage.py命令的startdash/startpanel命令选项
│   ├── messages.py # 消息通信
│   ├── middleware # 中间件,用来处理收到的异常和网页请求及回应需要的附加的动作
│   ├── notifications.py
│   ├── __pycache__
│   ├── site_urls.py # URL相关
│   ├── static # 静态文件包
│   ├── tables # table基类包
│   ├── tabs # tab基类包
│   ├── templates # 模板文件基类
│   ├── templatetags # 模板标签基类
│   ├── test # 测试包
│   ├── themes.py
│   ├── utils # 实用工具包
│   ├── version.py # 版本信息
│   ├── views.py # 视图
│   └── workflows # 工作流机制包
├── openstack_dashboard # horizon各个面板的具体实现代码
│   ├── api # 与外部api交互的方法和接口对象
│   ├── conf # nova、cinder等API访问权限控制,叫 xxx_policy.json,里面定义了鉴权用的rules
│   ├── context_processors.py # 上下文处理器
│   ├── contrib
│   ├── dashboards # Horizon界面展示各个模块实现目录
│  │   ├── admin # 管理员界面,管理登录后可见,左侧的管理员面板
│   │   │   ├── instances # 云主机管理界面
│   │   │   │   ├── forms.py # form表单实现
│   │   │   │   ├── __init__.py
│   │   │   │   ├── panel.py # 在应用程序中注册面板并定义面板属性
│   │   │   │   ├── __pycache__
│   │   │   │   ├── tables.py # table实现
│   │   │   │   ├── tabs.py # tab实现
│   │   │   │   ├── templates # 云主机html界面模板
│   │   │   │   ├── tests.py # 测试
│   │   │   │   ├── urls.py # url映射,描述了当浏览器网址指向那一级目录
│   │   │   │   └── views.py # url映射的视图,包含页面的业务逻辑,该文件里的函数通常叫做视图
│   │   │   ├── dashboard.py # 使用Horizon注册应用程序并设置dashboard属性
│  │   ├── identity # 项目、用户管理界面
│  │   ├── __init__.py
│  │   ├── project # 普通用户登录后看到的项目面板
│  │   ├── __pycache__
│  │   ├── settings # 设置界面,右上角的设置面板,里面可设置语言、时区、更改密码
│   ├── django_pyscss_fix
│   ├── enabled # 控制导航加载哪些模块显示出来
│   ├── exceptions.py # 异常处理
│   ├── hooks.py
│   ├── __init__.py
│   ├── karma.conf.js
│   ├── local # 本地配置文件
│   ├── locale # 本地国家化语言包
│   ├── management # 定义安装apache、horizon等是配置文件的模板文件
│   ├── policy.py # 策略
│   ├── __pycache__
│   ├── settings.py # 设置
│   ├── static # 静态包
│   ├── templates # 模板包
│   ├── templatetags # 模板标签包
│   ├── test # 测试包
│   ├── themes
│   ├── theme_settings.py
│   ├── urls.py # URL模型
│   ├── usage # 概况页面资源统计实现包
│   ├── utils # 工具包
│   ├── views.py
│   ├── wsgi # wsgi包
│   └── wsgi.py

使用Horizon构建dashboard

介绍如何使用Horizon中的各种组件来构建示例dashboard和带有tab的panel,该tab包含一个包含后端数据的表。【参考官方实例】

例如,我们将创建一个新的“My Dashboard”的Dashboard,其中“My Panel”的Panel具有“Instances Tab” 标签。该tab标签卡有一个表,其中包含Nova实例(instances)API提取的数据。

创建 Dashboard
  • 快速方式

    Horizon提供自定义管理命令,创建典型的Dashboard基础结构。在Horizon根目录中运行以下命令。它生成了需要的大部分样板代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $ mkdir openstack_dashboard/dashboards/mydashboard
    $ tox -e manage -- startdash mydashboard \
    --target openstack_dashboard/dashboards/mydashboard
    $ mkdir openstack_dashboard/dashboards/mydashboard/mypanel
    $ tox -e manage -- startpanel mypanel \
    --dashboard=openstack_dashboard.dashboards.mydashboard \
    --target=openstack_dashboard/dashboards/mydashboard/mypanel

    你会注意到目录mydashboard会自动填充与dashboard相关的文件,mypanel目录会自动填充与面板相关的文件。

  • 目录结构
    如果使用tree mydashboard命令列出openstack_dashboard/ dashboards中的mydashboard目录,你将看到如下所示的目录结构:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    .
    ├── dashboard.py
    ├── __init__.py
    ├── mypanel
    │   ├── __init__.py
    │   ├── panel.py.tmpl
    │   ├── templates
    │   │   └── mypanel
    │   ├── tests.py.tmpl
    │   ├── urls.py.tmpl
    │   └── views.py
    └── templates
    └── mydashboard

    不处理静态目录或tests.py文件,保持原样,有了其他文件和目录,我们可以继续添加我们自己的dashboard。

  • 定义dashboard
    打开dashboard.py文件,会注意到已自动生成以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from django.utils.translation import ugettext_lazy as _
    import horizon
    class Mydashboard(horizon.Dashboard):
    name = _("My dashboard")
    slug = "mydashboard"
    panels = () # Add your panels here.
    default_panel = '' # Specify the slug of the dashboard's default panel.
    horizon.register(Mydashboard)

如果你希望dashboard名称是其他名称,则可以更改dashboard.py文件中的name属性。 例如,你可以将其更改为“My Dashboard”

dashboard类通常包含name属性(dashboard的显示名称),slug属性(可由其他组件引用的内部名称),panels列表,默认panel等。
创建 panel

我们将创建一个panel并将其命名为My Panel。

  • 目录结构

    如上所述,openstack_dashboard/dashboards/mydashboard下的mypanel目录应如下所示:

    1
    2
    3
    4
    5
    6
    .
    ├── __init__.py
    ├── panel.py
    ├── templates
    │   └── mypanel
    └── views.py

    上面引用的panel.py文件具有特殊含义。在dashboard中,dashboard类的panels属性中列出的任何模块名称都将通过查找相应目录中的panel.py文件自动发现。
    打开panel.py文件,自动生成的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from django.utils.translation import ugettext_lazy as _
    import horizon
    from openstack_dashboard.dashboards.mydashboard import dashboard
    class Mypanel(horizon.Panel):
    name = _("My panel")
    slug = "mypanel"
    dashboard.Mydashboard.register(Mypanel)

    如果你希望Panel名称是其他名称,则可以更改panel.py文件中的name属性。 例如,可以将其更改为”My panel”。
    再次打开dashboard.py文件,在Mydashboard类上面插入以下代码。此代码定义了Mygroup类并添加了一个名为mypanel的面板:

    1
    2
    3
    4
    5
    class Mydashboard(horizon.Dashboard):
    name = _("My Dashboard")
    slug = "mydashboard"
    panels = (Mygroup,) # Add your panels here.
    default_panel = 'mypanel' # Specify the slug of the default panel.

    完成的dashboard.py文件应如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    from django.utils.translation import ugettext_lazy as _
    import horizon
    class Mygroup(horizon.PanelGroup):
    slug = "mygroup"
    name = _("My Group")
    panels = ('mypanel',)
    class Mydashboard(horizon.Dashboard):
    name = _("Mydashboard")
    slug = "mydashboard"
    panels = (Mygroup, ) # Add your panels here.
    default_panel = 'mygroup' # Specify the slug of the dashboard's default panel.
    horizon.register(Mydashboard)
Tables, Tabs, and Views

将从table开始,将其与tabs相结合,然后从各个部分构建我们的视图。

  • 定义table

    Horizon提供了一个SelfHandlingForm DataTable类,它简化了大多数向最终用户显示数据的形式。我们只是在这里简单举例,但它具有大量的功能。在mypanel目录下创建一个tables.py文件并添加以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    from django.utils.translation import ugettext_lazy as _
    from horizon import tables
    class InstancesTable(tables.DataTable):
    name = tables.Column("name", verbose_name=_("Name"))
    status = tables.Column("status", verbose_name=_("Status"))
    zone = tables.Column('availability_zone',
    verbose_name=_("Availability Zone"))
    image_name = tables.Column('image_name', verbose_name=_("Image Name"))
    class Meta(object):
    name = "instances"
    verbose_name = _("Instances")

    创建了一个tables.DataTable子类,并定义了我们想要检索显示的四列数据。这些列中的每一列显示的数据为Instances对象的属性,即定义的第一个属性值,verbose_name参数表示该列在table表格thead中的中文名字。
    最后,我们添加了一个Meta类,描述instances表的元对象,即表示数据来自instances数据表。

  • 定义table 动作
    Horizon提供了三种类型的基本操作类,可以在表的数据上使用:

    • Action:表示可以对此表的数据执行的操作。
    • LinkAction:表示操作只是一个链接而不是POST表单。
    • FilterAction:表示表的过滤器操作的基类。

    还有其他操作是基本Action类的扩展:

    • BatchAction:对一个或多个对象执行批处理操作的表操作。此操作不应要求基于每个对象的用户输入。
    • DeleteAction:用于对表数据执行删除操作的表操作。
    • FixedFilterAction:带固定按钮的过滤器操作。

表中添加一个过滤器操作,显示包含在过滤器字段中输入的字符串的行。编辑tables.py文件:

1
2
class MyFilterAction(tables.FilterAction):
name = "myfilter"

上面指定的操作将默认filter_type为“query”。这意味着过滤器将使用客户端表排序。

将该操作添加到表的表操作中:

1
2
3
4
class Meta(object):
name = "instances"
verbose_name = _("Instances")
table_actions = (MyFilterAction,)

  • 定义tabs

    上面定义了一个表,可以显示数据。我们可以直接从这里看到一个view视图,但在里,我们还将使用horizon的TabGroup类,来定义一个tab,通过一个tab来显示视图。
    在mypanel目录下创建一个tabs.py文件,创建一个包含一个tab的tab group:

    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
    from django.utils.translation import ugettext_lazy as _
    from horizon import exceptions
    from horizon import tabs
    from openstack_dashboard import api
    from openstack_dashboard.dashboards.mydashboard.mypanel import tables
    class InstanceTab(tabs.TableTab):
    name = _("Instances Tab")
    slug = "instances_tab"
    table_classes = (tables.InstancesTable,)
    template_name = ("horizon/common/_detail_table.html")
    preload = False
    def has_more_data(self, table):
    return self._has_more
    def get_instances_data(self):
    try:
    marker = self.request.GET.get(
    tables.InstancesTable._meta.pagination_param, None)
    instances, self._has_more = api.nova.server_list(
    self.request,
    search_opts={'marker': marker, 'paginate': True})
    return instances
    except Exception:
    self._has_more = False
    error_message = _('Unable to get instances')
    exceptions.handle(self.request, error_message)
    return []
    class MypanelTabs(tabs.TabGroup):
    slug = "mypanel_tabs"
    tabs = (InstanceTab,)
    sticky = True

    这个tab标签变得有点复杂。该tab用于处理上面定义的数据表(及其所有相关特性),并且还使用preload属性指定默认情况下不应加载此tab,相反,当有人单击它时,它将通过AJAX加载,在大多数情况下节省了API调用的时间。
    此外,表的显示由一个可重用的模板horizon/common/_detail_table.html处理。添加了一些简单的分页代码来处理大量实例列表。
    最后,介绍了horizon中错误处理的机制,即是异常捕捉。horizon.exceptions.handle()函数是一种集中式错误处理机制,它可以处理来自API的异常的所有错误以及不一致。

  • 在视图中将它们添加上去
    打开views.py文件,自动生成的代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from horizon import views
    class IndexView(views.APIView):
    # A very simple class-based view...
    template_name = 'mydashboard/mypanel/index.html'
    def get_data(self, request, context, *args, **kwargs):
    # Add data to the context here...
    return context

    在本例中:导入正确的包后,已完成的views.py文件现在如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from horizon import tabs
    from openstack_dashboard.dashboards.mydashboard.mypanel \
    import tabs as mydashboard_tabs
    class IndexView(tabs.TabbedTableView):
    tab_group_class = mydashboard_tabs.MypanelTabs
    template_name = 'mydashboard/mypanel/index.html'
    def get_data(self, request, context, *args, **kwargs):
    # Add data to the context here...
    return context
创建 URLs

自动生成的urls.py文件如下:

1
2
3
4
5
6
7
8
from django.conf.urls import url
from openstack_dashboard.dashboards.mydashboard.mypanel import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
]

创建 template

在mydashboard/mypanel/templates/mypanel目录中创建/打开index.html文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "My Panel" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("My Panel") %}
{% endblock page_header %}
{% block main %}
<div class="row">
<div class="col-sm-12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

这里提供了自定义页面标题,并呈现了视图提供的tab组。

接下去将它集成到我们的OpenStack Dashboard站点中。

启用并显示dashboard

为了使My Dashboard与Project或Admin等现有dashboard一起显示,需要在openstack_dashboard/enabled下创建一个名为_50_mydashboard.py的文件,并添加以下内容:

运行
1
tox -e runserver -- 0.0.0.0:9000
坚持原创技术分享,您的支持将鼓励我继续创作!