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便會出現,如下圖所示,在實例未啓動前,創建快照按鈕不會出現
在這裏插入圖片描述
實例啓動之後,創建快照按鈕便會出現,點擊創建快照按鈕,彈出創建快照小窗口,如下圖所示:
在這裏插入圖片描述
設置好快照名稱,點擊創建快照,會跳轉進入快照創建頁面,完成創建

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