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