openstack开发实践(三):Dashboard开发

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面板
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便会出现,如下图所示,在实例未启动前,创建快照按钮不会出现
在这里插入图片描述
实例启动之后,创建快照按钮便会出现,点击创建快照按钮,弹出创建快照小窗口,如下图所示:
在这里插入图片描述
设置好快照名称,点击创建快照,会跳转进入快照创建页面,完成创建

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章