Dashboard概述
Dashboard是openstack中提供的一个web前端控制台,以此来展示openstack的功能。Dashboard是一个基于Django Web Framework开发的标准的Python WSGI程序。关于Django的应用开发在上一篇教程openstack开发实践(三)中已经进行了详细的介绍https://blog.csdn.net/weixin_41977332/article/details/104324159。Dashboard将页面上的所有元素模块化,网页中一些常见元素(如表单,表格,标签页)全部被封装成Python类,每个组件都有自己对应的一小块HTML模板,当渲染整个页面的时候,Dashboard先查找当前页面有多少组件,然后将各个组件分别进行渲染变成一段HTML片段,最后拼装成一个完整的HTML页面,返回给浏览器
Dashboard UI整体结构
下图为一个标准的Dashboard页面。一个dashboard面板可以分为三层:
Dashboard—>PanelGroup—>Panel,UI结构最上面为Header,左上边为logo,然后是Dashboard,包括项目,管理员,身份管理,(Mydashboard为我自己开发并添加上去的dashboard,官方版本的仅有项目,管理员,身份管理)。每一个Dashboard都可以理解为是Django中的一个APP,Django中的APP可以理解成对业务逻辑模块化的一种手段,里面可以包含自己独有的URL设定、模板和业务逻辑代码。每个Dashboard下面有若干个PanelGroup,比如项目下有计算和网络两个PanelGroup;每个PanelGroup下有若干个Panel,比如计算下有概况,实例,卷,镜像,密钥对这几个Panel。点开Panel之后右侧部分显示的是Panel Body,Panel Body中显示的是Data Table View。除了Data Table View之外还有一种是Tab View样式,如图系统信息所示
Dashboard源码结构
通过devstack安装的openstack中的dashboard源码位于/opt/stack/horizon目录下,主要包含两个代码文件夹:horizon和openstack_dashboard。horizon中主要是一些在Django基础上编写的通用组件,包括表格(table),标签页(tab),表单(form),导航(browser),工作流(flow)。这些代码和openstack的具体业务逻辑没有什么关系,如果要做一个新的Django项目,理论上也可以复用这些代码。horizon/base.py中还实现了一套Dashboard/Panel机制,使得Horzion面板上所有的Dashboard都是“可插拔”的,所有的Panel都是“动态加载”的。
openstack_dashboard/dashboards/中是各个面板的具体实现代码,其中包括各个面板的模板文件和后端Service交互的业务逻辑代码等。从中可以看出之前介绍的面板中各个dashboard即项目(project),管理员(admin),身份管理(identity),后续我们自己编写的dashboard即Mydashboard也要在这里创建一个文件夹并放入部分代码。在dashboards/enabled目录中的py文件则是告诉horizon在渲染Dashboard时载入这些Panel。enabled目录下的Dashboard和Panel是按照Python文件名的字典序添加的,所以可以通过_1000_.py,_2000_.py的命名来控制文件名的字典序。在python文件中可以找到页面上对应的project、admin等对应的调用。
Mydashboard开发
我们这里创建的Mydashboard如下图所示,可以展示虚拟机实例并且给正在运行的虚拟机创建快照。主要分为两大步骤:自定义Dashboard和Panel,添加创建快照操作
自定义Dashboard和Panel
创建Dashboard和Panel
我们使用官方提供的run_test.sh脚本来协助创建Dashboard和Panel,创建Dashboard步骤如下所示:
su - stack
cd horizon
mkdir openstack_dashboard/dashboards/mydashboard
./run_tests.sh -m startdash mydashboard --target openstack_dashboard/dashboards/mydashboard/
创建Panel的步骤如下所示
mkdir openstack_dashboard/dashboards/mydashboard/mypanel
./run_tests.sh -m startpanel mypanel --dashboard=openstack_dashboard.dashboards.mydashboard --target=openstack_dashboard/dashboards/mydashboard/mypanel
编写代码
添加PanelGroup和Panel
编辑mydashboard/dashboard.py,在Mydashboard下添加PanelGroup,名称为Mygroup,在Mygroup下添加一个Panel,名字为Mypanel:
from django.utils.translation import ugettext_lazy as _
import horizon
class Mygroup(horizon.PanelGroup):
name = _("My Group")
slug = "mygroup"
panels = ('mypanel',)
class Mydashboard(horizon.Dashboard):
name = _("Mydashboard")
slug = "mydashboard"
panels = (Mygroup,) # Add your panels here.
default_panel = 'mypanel' # Specify the slug of the dashboard's default panel.
horizon.register(Mydashboard)
定义tables
接下来添加tables,tabs和views。首先定义tables,在mydashboard/mypanel目录下创建tables.py,内容如下:
from django.utils.translation import ugettext_lazy as _
from horizon import tables
class MyFilterAction(tables.FilterAction):
name = "myfilter"
class InstancesTable(tables.DataTable):
name = tables.Column('name', verbose_name=_("Name"))
status = tables.Column('status', verbose_name=_("Status"))
availability_zone = tables.Column('OS-EXT-AZ:availability_zone', verbose_name=_("Availability Zone"))
instance_name = tables.Column('OS-EXT-SRV-ATTR:instance_name', verbose_name=_("Instance Name"))
image_name = tables.Column('image_name', verbose_name=_("Image Name"))
class Meta:
name = "instances"
verbose_name = _("Instances")
table_actions = (MyFilterAction,)
上面的程序,创建了一个table子类InstancesTable,定义了四列,每列定义了它访问实例类的属性作为第一个参数,因为希望所有的事情都是可以被翻译的,即面板可以被汉化或翻译为其他语言,我们给每列赋予一个verbose_name以标记它们可以被翻译。Horzion提供了三种基本的action:Action、LinkAction、FilterAction,另外还提供了四种扩展的action:BatchAction、DeleteAction、UpdateAction、FixedFilterAction。这些都在horizon/tables/actions.py。我们这里添加了一个FilterAction,即tables.py中的MyFilterAction类,然后将这个action加入table中。
定义tabs
有了tab就可以接受数据,并可以直接得到一个视图。在mypanel目录下新建tabs.py文件,其内容如下:
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): #函数中间的名字(instances)和tables.py中class Meta里的name对应
try:
instances, self._has_more = api.nova.server_list(self.request)
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
该tabs处理tables的数据以及所有有关的特性,同时它可以使用preload属性来指定这个tab是否被加载,默认情况下为不加载。当单击它时,它将通过AJAX方式加载,在绝大多数情况下保存我们的API调用。
整合views
修改mydashboard/mypanel下的views.py
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
在Django中,视图就是一个Python函数,它接收httpRequest的参数,返回httpResponse对象。Django得到这个返回对象后,将它转换成对应的HTTP响应,显示网页内容。
在mydashboard/mypanel/templetas下的index.html中加入如下内容:
{% block main %}
<div class="row">
<div class="col-sm-12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}
添加Enable的文件
在openstack_dashboard/enabled下添加文件5000_mydashboard.py:
# The slug of the dashboard to be added to HORIZON['dashboards']. Required.
DASHBOARD = 'mydashboard'
# A list of applications to be added to INSTALLED_APPS.
ADD_INSTALLED_APPS = [
'openstack_dashboard.dashboards.mydashboard',
]
添加创建快照操作
之前我们已经添加了一个FilterAction,这里我们添加一个LinkAction创建快照。主要步骤如下:
定义view
进入mydashboard/mypanel/templates/mypanel,添加create_snapshot.html和_create_snapshot.html,实现创建快照的小窗口,内容分别如下:
create_snapshot.html:
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Snapshot" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Create a Snapshot") %}
{% endblock page_header %}
{% block main %}
{% include 'mydashboard/mypanel/_create_snapshot.html' %}
{% endblock %}
_create_snapshot.html:
{% extends "horizon/common/_modal_form.html"%}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Snapshots preserve the disk state of a running instance:" %}</p>
{% endblock %}
进入mydashboard/mypanel目录,创建forms.py文件,负责提交创建快照申请到nova中:
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from openstack_dashboard import api
class CreateSnapshot(forms.SelfHandlingForm):
instance_id = forms.CharField(label=_("Instance ID"),widget=forms.HiddenInput(),required=False)
name = forms.CharField(max_length=255, label=_("Snapshot Name"))
def handle(self, request, data):
try:
snapshot = api.nova.snapshot_create(request,data['instance_id'],data['name'])
return snapshot
except Exception:
exceptions.handle(request,_('Unable to create snapshot.'))
在同一目录下更新views.py文件,定义快照操作相关的操作,内容如下:
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import tabs
from horizon import exceptions
from horizon import forms
from horizon.utils import memoized
from openstack_dashboard import api
from openstack_dashboard.dashboards.mydashboard.mypanel import forms as project_forms
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
class CreateSnapshotView(forms.ModalFormView):
form_class = project_forms.CreateSnapshot
template_name = 'mydashboard/mypanel/create_snapshot.html'
success_url = reverse_lazy("horizon:project:images:index")
modal_id = "Create_snapshot_modal"
modal_header = _("Create Snapshot")
submit_label = _("Create Snapshot")
submit_url = "horizon:mydashboard:mypanel:create_snapshot"
@memoized.memoized_method
def get_object(self):
try:
return api.nova.server_get(self.request,self.kwargs["instance_id"])
except Exception:
exceptions.handle(self.request,_("Unable to retrieve instance."))
def get_initial(self):
return {"instance_id": self.kwargs["instance_id"]}
def get_context_data(self, **kwargs):
context = super(CreateSnapshotView,self).get_context_data(**kwargs)
instance_id = self.kwargs['instance_id']
context['instance_id'] = instance_id
context['instance'] = self.get_object()
context['submit_url'] = reverse(self.submit_url,args=[instance_id])
return context
定义URL
编辑mypanel目录下的urls.py文件,更新内容如下:
from django.conf.urls import url
from openstack_dashboard.dashboards.mydashboard.mypanel import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<instance_id>[^/]+)/create_snapshot/$',views.CreateSnapshotView.as_view(),name='create_snapshot'),
]
定义Action
编辑mypanel目录下的tables.py文件,添加创建快照Actions,文件最终内容如下:
from django.utils.translation import ugettext_lazy as _
from horizon import tables
def is_deleting(instance):
task_state = getattr(instance, "OS-EXT-STS:task_state", None)
if not task_state:
return False
return task_state.lower() == "deleting"
class CreateSnapshotAction(tables.LinkAction):
name = "snapshot"
verbose_name = _("Create Snapshot")
url = "horizon:mydashboard:mypanel:create_snapshot"
classes = ("ajax-modal",)
icon = "camera"
def allowed(self,request,instance=None):
return instance.status in ("ACTIVE") and not is_deleting(instance)
class MyFilterAction(tables.FilterAction):
name = "myfilter"
class InstancesTable(tables.DataTable):
name = tables.Column('name', verbose_name=_("Name"))
status = tables.Column('status', verbose_name=_("Status"))
availability_zone = tables.Column('OS-EXT-AZ:availability_zone', verbose_name=_("Availability Zone"))
instance_name = tables.Column('OS-EXT-SRV-ATTR:instance_name', verbose_name=_("Instance Name"))
image_name = tables.Column('image_name', verbose_name=_("Image Name"))
class Meta:
name = "instances"
verbose_name = _("Instances")
table_actions = (MyFilterAction,)
row_actions = (CreateSnapshotAction,)
完成开发和最终效果展示
完成上述代码添加后,运行horizon/manage.py,并重启apache2服务sudo /etc/init.d/apache2 restart。此时Mydashboard便会出现,如下图所示,在实例未启动前,创建快照按钮不会出现
实例启动之后,创建快照按钮便会出现,点击创建快照按钮,弹出创建快照小窗口,如下图所示:
设置好快照名称,点击创建快照,会跳转进入快照创建页面,完成创建