本文出自我的同事兼基友@monsterxx03 之手,本人稍作潤色
Horizon Is Easy, Horizon Is Complex
如果要用一句話來概括Openstack Dashboard項目Horizon:它是一個基於django webframework開發的標准的python wsgi程序。如果要再加一句廢話,它一般會運行在webserver(apache/nginx)上。
Why Is It Easy?
說Horizon簡單,是指它的部署架構簡單,就是個單純的基於django的網站,后台甚至沒有使用數據庫。不像Swift,Nova這些項目,有非常多的組件(xxx-server,xxx-api…),理清楚各個組件之間的關系,數據流的走向都要費一番功夫。Horizon用到的技術也比較傳統,沒有異步非阻塞,消息隊列,websocket這些比較時髦的東西(目前沒有),前端也只是bootstrap+jquery+less,沒有什么很前衛的東西。
用戶從登陸到看到虛擬機列表的這個過程的數據流走向:
Why Is It Complex?
Horizon的復雜性分成兩方面看:
-
Horizon需要和幾乎所有的OpenStack service api打交道, 而且各個服務可能就還有多個api版本,api多版本的兼容不是都能在client里完成的(比如Keystone),Horizon還需要針對各個api的多版本做兼容。
-
Horizon代碼本身很復雜,抽象,利用了django相當多的高級特性,可以說把oop的概念運用到了極致,但這也為 過度自定制Horizon帶來了麻煩。
Design Philosophy
Horizon是基於django開發的,要說Horizon的設計,不得不說django,
Django focuses on automating as much as possible and adheres to the DRY(Don't Repat Yourself) principle.
django的設計專注於代碼的高度可重用,信奉DRY原則,一切面向對象,而Horizon可以說高度match了django的設計風格,good or bad就見仁見智了。
先說一點web開發的常識,網站程序基本有三部分組成,業務邏輯代碼(Python),靜態文件(js/css),模板(Python中的jinja,mako,nodejs中有jade), 用戶向webserver發起請求之后,server程序找到當前url對應的模板,填充模板變量(輸出成字符串形式的html源碼),返回給瀏覽器,瀏覽器渲染頁面。
如果是簡單的程序,一個頁面就對應一個模板,非常簡單。一般一個網站有自己的統一風格,所有頁面都會有一些相同的元素,每個頁面單獨寫一遍很累贅,也不好維護,所以一般模板語言都有繼承(extend),插入(include)等特性,來提高頁面的復用率。
Horizon做得就更徹底一些,它將頁面上所有元素模塊化,網頁中一些常見元素,表單,表格,標簽頁,全部封裝成Python類,每個組件有自己對應的一小塊html模板.當渲染整個頁面的時候,Horizon先找到當前頁面有多少組件,將各個組件分別進行渲染變成一段html片段,最后拼裝成一個完整的html頁面,返回瀏覽器。
簡單總結Horizon的特點:
-
頁面元素模塊化
-
子面板可插拔
-
All in One(從部署上來說,Horizon只有它自己這一個組件)
Dive into Horizon
Some Concepts
Horizon這套面板的設計分成三層:Dashboard → PanelGroup → Panel
Horizon中現有的dashboard有4個:
-
project 普通用戶登陸后看到的 項目面板
-
admin 管理登陸后可見,左側的 管理員面板
-
settings 右上角的設置面板,里面可設置語言,時區,更改密碼
-
router(配置文件中將profile_support打開可見),ciso nexus 1000v的管理面板
每一個dashboard都是django中的一個app,django中的app可以理解成對業務邏輯模塊化的一種手段,里面可以包含自己獨有的url設定,模板,和業務邏輯代碼.
每個dashboard下定義了一系列的PanelGroup,虛擬機管理對應到界面上就是一個PanelGroup(Manage Compute), 里面有一系列的子panel(Overview, Instances, Volumes…)。Swift,heat,neutron的管理面板各自都是一個PanelGroup,底下有各自的子panel.
Some Code
Horizon的源碼中,包含兩個代碼文件夾
-
horizon
-
openstack_dashboard
Horizon這個包是一些在django基礎上寫的通用組件,表格(table),標簽頁(tab),表單(form),面包屑導航(browser),工作流(workflow),這些代碼和openstack的具體業務邏輯沒有什么關系,如果做一個新的django項目,理論上可以復用Horizon這個包中的代碼。horizon/base.py中還實現了一套dashboard/panel機制,使得Horizon面板上所有的dashboard都是”可插拔”的,所有的panel都是”動態加載”的。
openstack_dashboard/dashboards/中是各個面板的具體實現代碼,其中包括各個面板的模板文件, 和后端service交互的業務邏輯代碼等。
FAQ
Horizon怎么實現dashboard的可插拔?
之前說過,Horizon中的dashboard就是django的app,在openstack_dashboard/settings.py中的INSTALLED_APP變量定義了目前已有的四個dashboard:
-
openstack_dashboard.dashboards.project
-
openstack_dashboard.dashboards.admin
-
openstack_dashboard.dashboards.settings
-
openstack_dashboard.dashboards.router
如果自己按照Horizon自定制dashboard的流程寫了新的dashboard也需要加到這個配置文件中,新面板就能正常顯示。
每個dasboard是怎么找到自己擁有的panel的?
每個dashboard模塊下都有一個dasbboard.py文件里面定義了屬於當前dashboard的PanelGroup,和各個PanelGroup下的Panel,在Horizon模塊被導入的時候會去依次遍歷Dasboard→PanelGroup→Panel,所有的dashboard注冊到Horizon命名空間下,各個panel注冊到自己的dashboard命名空間下。
每個panel是怎么找到自己的模板的?
這需要理解django中的template loader概念,簡單的說,template loader就是一個處理request的視圖尋找自己的模板文件路徑的方法,django自帶了好幾種template加載機制,Horizon用到了filesystem,app_directories這兩種,另外自己自定義了一種(horizon.loaders.TemplateLoader),loader的定義在openstack_dashboard.settings中的TEMPLATED_LOADERS變量。
TEMPLATED_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
'horizon.loaders.TemplateLoader',
)
舉例:
當點擊左側project(這是個dashboard)下的instance(這是個panel)標簽時,頁面會跳轉到http://localhost/project/instances這個url,panel在的視圖類中寫了template_name=“project/instances/index.html”,查找這個文件的順序是:
-
先找openstack_dashboard/templates
-
沒找到再找openstack_dashboard/dashboards/project/templates/,這個目錄不存在,還是沒找到。
-
最后找openstack_dashboard/dashboards/project/instances/templates, 找了index.html這個文件。
怎么顯示swift的面板?怎么顯示heat的面板?
這里很好的體現了Horizon的動態特性,顯不顯示某panel,實際上並不由Horizon控制,而是由Keystone控制。一個后端的webservice需要集成進openstack的話,第一步是在Keyston處登記,在Keyston的service,endpoint這兩張表內寫入該service的的一些元數據信息(包括url地址)。
Horizon控制面板是否顯示用了一套叫permission的機制,比如Container(swift的panel)要顯示的條件是”openstack.services.object-store”。用戶的登陸后的token中又service catalog信息,catalog中必須包含object-store這個service,滿足,container這個panel就會注冊到project這個dashboard之下,頁面上就能顯示,如果Keyston中沒有swift的catalog信息,Horizon就不會注冊container這個panel,swift的面板就不會顯示。heat同理。
要了解permission的具體實現請看openstack_auth/backend.py 模塊
普通用戶看不到admin面板,這個是怎么實現的?
還是用到了上個問題提到的permission,Admin dashboard這個類中寫的permission是”openstack.roles.admin”,則admin這個面板顯示的條件是當前登錄的用戶有admin這個role,滿足的用戶就能看到admin dashbaord.
Horizon處理登錄部分的代碼在哪?
參看openstack_auth部分
Customize Horizon
自定義css
要對Horizon的css樣式做自定制,可以修改openstack_dashboard/static/less 目錄下的less文件。
自定義js
要對Horizon中js的行為做定制,可以修改horizon/static/horizon/js/ 目錄下的文件,Horizon的js文件都是針對具體某個頁面組件做的行為定制,比如form提交時的行為,tab切換的行為,所以都放在Horizon目錄下而不在openstack_dashboard目錄下。
添加新的dashboard和panel
Horizon自帶了兩條命令,方便快速得生成基礎代碼:
-
`python manage.py startdash test_dash` 自動生成一個叫test_dash的dashboard,之后將test_dash寫入settings中的INSTALLED_APP中,Horizon就能加載此新面板
-
`python manage.py startpanel test_panel -d openstack_dashboard.dashboards.admin –target auto` 會在Horizon的admin dashboard下生成一個叫test_panel的子面板
Event Driven Horizon
Horizon目前的實現是比較傳統的全刷新網站,openstack中有不少操作是異步的api,比如創建虛擬機,snapshot,volume等等。這些api被調用后資源會進入一個狀態遷移的過程,Horizon在前端頁面上實現狀態的實時刷新,用的是ajax輪訓的方式,這種方式效率比較低,也有延遲,目前也有bp要為Horizon引進real time的特性,相關bp:
有一個實現草案:https://review.openstack.org/#/c/40198/1
原理是通過sock.io(python版基於tornado的ioloop實現)再寫一個websocket的server.同時Horizon后台添加消息隊列支持,監聽openstack集群中的消息,監聽到了自己需要的消息之后,通知websocket server,server再通知瀏覽器中的js.
不過這樣做有一些問題,server上需要躲開一個端口專門處理websocket請求,還需要一個daemon監聽消息隊列的消息,這會增加Horizon的部署復雜度。社區決定將real time的實現放到I版做,這個實現草案中將整個Horizon本身也跑在了tornado server上,這里的性能問題有待商榷(主要是對eventlet的monkey_patch的原理有疑惑)。
吐槽
本來想寫基於Horizon開發會帶來的不便利的地方,想了一下,先寫些離題的東西。
首先噴一下django的DRY原則,很多程序員都追求自己的代碼里沒有冗余的代碼,希望系統像一個精密的機械每個齒輪都緊緊得咬合,沒有一個多余的零件,也不要出現兩個相同的零件,這就是所謂的DRY(don't repeat yourself)。但是也有一句話流傳的很廣:“DRY有毒”。DRY是不是有毒,我覺得要看寫的是什么程序,如果就是一個在后台默默跑着的server,它和外部交互的協議都是定死的,那把接口實現之后無論怎么重構代碼,追求架構上的DRY都不過分。
但網站這種項目,終端消費者是人而不是其他程序,它和外部交互的協議就是html頁面。人是最喜新厭舊的,所以會出現網站改版這種事,說白了網站程序它和外部的交互協議三天兩頭都有可能變化。當你費盡奇淫巧計,用上各種Design Pattern,OOP設計,最后老板說:“那什么,我想把這個頁面的按鈕做成大號的,而且點了之后框給我跳兩下再出來”,就開始郁悶了,你和boss說,按照我的優良OOP設計,所有頁面上的按鈕都繼承自一個BaseButton,他們的動畫效果都是預先定義好的,按鈕點了之后對話框能從上下左右出來,就是不能跳兩下再出來,可以改,但是會破壞我原本的優良設計。喂喂,根本沒人會來理你,看頁面的人在乎的只是這個按鈕而已。
可能你會說,這個例子里,我設計的按鈕類不夠強大,它應該寫的更加抽象,子類的行為要更加靈活。但要知道,程序員能實現的架構的復雜度與層次性和個人的經驗,項目所給的時間有相當大的關系。
說白了,就是水平不夠,偏要去追求什么沒有一行重復代碼這種事情,而且給的時間只有個把月,最后的代碼很DRY但是不夠靈活,只會讓自己陷入困境.把握程序的優雅與靈活之間的平衡是一件很難的事情,有時候要量力而行。
再回到Horizon這個項目,讓我評價Horizon的代碼架構本身(去除一些pep8的小問題,我個人並不是很待見pep8),我的評價是Excellent!反正在一些開源的基於django的程序的實現里,見過代碼行數更多的,但沒見過代碼寫的更漂亮的,至少是沒見過比它用django用的更牛的。
但是Horizon也不是一開始就是這個架構,我記憶里E版的Horizon里那個DataTable的實現真的是百轉千回,死活看不懂。現在在H版里的實現將DataTable這個組件進行了很詳細的拆分,仍舊很復雜,但比以前的實現要清楚的多。
我曾經將Horizon的commit記錄reset到第一條,看過它早期的一些實現變化過程,它早期的代碼那叫個簡陋,各種臟,各種修修補補,最早的commit記錄是2011年1月,一直到現在2013年10月,將近3年的開發時間,一般公司,如果說我們做個面板,做個三年,最后做出來長Horizon這樣,那肯定會有人跳出來說:“丫我拿php寫一個,1個禮拜就能搞定,他媽你搞上三年,php是最好的語言,php萬歲!”.
但Horizon的的價值對一般人來說在openstack api的可視化體現,對開發人員來說是優秀代碼的學習范例,如果你用一種傳統的方式去開發一個openstack面板,他可能可以把前端做的很炫,讓Horizon顯得很土鱉,但代碼100%比不上Horizon,我的意見是你用django開發,后台寫成Horizon這樣,基本可以說是優秀到頭了。