Understand TOSCA Template Engine

此篇重點著重於 TOSCA Template Engine,將會從程式碼帶到它是如何把所有 yaml file 當中提到的 Module 引用進來的步驟。

前情提要

首先可以先來看看 MakeFile 的內容(但是因為 mcord 裡面沒有 exampleservice,且這正是我們的目的,把 exampleservice onboard 到 mcord 上),所以在此暫以 cord_pod/MakeFile 之中的 exampleservice 為例:

1
2
3
4
5
6
7
8
9
10
11
12
13
exampleservice: $(SERVICE_DIR)/exampleservice
@echo "[EXAMPLESERVICE]"
sudo cp id_rsa key_import/exampleservice_rsa
sudo cp id_rsa.pub key_import/exampleservice_rsa.pub
$(RUN_TOSCA_BOOTSTRAP) $(COMMON_DIR)/tosca/disable-onboarding.yaml
$(RUN_TOSCA_BOOTSTRAP) $(SERVICE_DIR)/exampleservice/xos/exampleservice-onboard.yaml
$(RUN_TOSCA_BOOTSTRAP) exampleservice-synchronizer.yaml
$(RUN_TOSCA_BOOTSTRAP) $(COMMON_DIR)/tosca/enable-onboarding.yaml
bash $(COMMON_DIR)/wait_for_onboarding_ready.sh $(XOS_BOOTSTRAP_PORT) services/exampleservice
bash $(COMMON_DIR)/wait_for_onboarding_ready.sh $(XOS_BOOTSTRAP_PORT) xos
bash $(COMMON_DIR)/wait_for_xos_port.sh $(XOS_UI_PORT)
$(RUN_TOSCA) exampleservice.yaml
sleep 60

這邊使用了 $(RUN_TOSCA_BOOTSTRAP) 來把四個 YamlFile 餵入,分別為以下四條指令:

  • $(COMMON_DIR)/tosca/disable-onboarding.yaml
  • $(SERVICE_DIR)/exampleservice/xos/exampleservice-onboarding.yaml
  • exampleservice-synchronizer.yaml
  • $(COMMON_DIR)/tosca/enable-onboarding.yaml

可以見到有兩個 Variable:$(RUN_TOSCA_BOOTSTRAP), $(COMMON_DIR)

RUN_TOSCA_BOOTSTRAP

Defined in service-profile/common/Makefile

1
2
3
4
# XOS_BOOTSTRAP_PORT = 81, XOS_UI_PORT = 80

RUN_TOSCA_BOOTSTRAP ?= python $(COMMON_DIR)/run_tosca.py $(XOS_BOOTSTRAP_PORT) $(ADMIN_USERNAME) $(ADMIN_PASSWORD)
RUN_TOSCA ?= python $(COMMON_DIR)/run_tosca.py $(XOS_UI_PORT) $(ADMIN_USERNAME) $(ADMIN_PASSWORD)

:::

run_tosca.py

1
2
3
4
5
6
7
8
9
10
11
12
13
port = int(sys.argv[1])
username = sys.argv[2]
password = sys.argv[3]
tosca_fn = sys.argv[4]

xos_auth=(username, password)

hostname = "127.0.0.1"
url = "http://%s:%d/api/utility/tosca/run/" % (hostname, port)

recipe = open(tosca_fn).read()

r = requests.post(url, data={"recipe": recipe}, auth=xos_auth)

其實就只是把 yaml 丟到一個 /api/utility/tosca/run 的 RESTful API,而這個 API 的位置位於 xos/xos/api/utility/toscaapi.py#35

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def post_run(self, request):
result = []

recipe = request.data.get("recipe", None)

sys_path_save = sys.path
try:
sys.path.append(toscadir)
from tosca.engine import XOSTosca
xt = XOSTosca(recipe, parent_dir=toscadir, log_to_console=False)
xt.execute(request.user)
except:
return Response( {"error_text": traceback.format_exc()}, status=500 )
finally:
sys.path = sys_path_save

toscaapi.pyLine 44 可以看到呼叫了一個 XOSTosca 的物件,這一篇紀錄將會針對這個類別研究。

XOSTosca

往上可以看到 XOSToscafrom tosca.engine import XOSTosca 引用的,所以接下來將會看一看 xos/xos/tosca/engine.py 去研究:

XOSTosca in engine.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class XOSTosca(object):
def __init__(self, tosca_yaml, parent_dir=None, log_to_console = False):
# TOSCA will look for imports using a relative path from where the
# template file is located, so we have to put the template file
# in a specific place.
if not parent_dir:
parent_dir = os.getcwd()

tmp_pathname = None
try:
(tmp_handle, tmp_pathname) = tempfile.mkstemp(dir=parent_dir, suffix=".yaml")
os.write(tmp_handle, tosca_yaml)
os.close(tmp_handle)

self.template = ToscaTemplate(tmp_pathname)
except:
traceback.print_exc()
raise
finally:
if tmp_pathname:
os.remove(tmp_pathname)

這一段程式碼裡面作的事情是:

  • 作一個 Tempfile,用以將讀進來的 raw 放進去 Tempfile #L24-28
    • tmp_handle 為 File descriptor,tmp_pathname 為 File Path
  • 如果有噴 Exception 就印出來
  • 最終把 tmpfile 移除掉(因為已經在 #L28 放到 self.template 這個 private variable 了)
1
2
3
4
5
6
7
8
9
10
11
12
13
self.log_to_console = log_to_console
self.log_msgs = []

self.compute_dependencies()

self.deferred_sync = []

self.ordered_nodetemplates = []
self.ordered_names = self.topsort_dependencies()
self.log("ordered_names: %s" % self.ordered_names)
for name in self.ordered_names:
if name in self.nodetemplates_by_name:
self.ordered_nodetemplates.append(self.nodetemplates_by_name[name])

這一段比較重要的有以下幾點,稍候將會分別說明其用途與意義:

  • self.compute_dependencies()
  • self.ordered_nodetemplates = []
  • self.ordered_names = self.topsort_dependencies()

compute_dependencies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def compute_dependencies(self):
nodetemplates_by_name = {}
for nodetemplate in self.template.nodetemplates:
nodetemplates_by_name[nodetemplate.name] = nodetemplate

self.nodetemplates_by_name = nodetemplates_by_name

for nodetemplate in self.template.nodetemplates:
nodetemplate.dependencies = []
nodetemplate.dependencies_names = []
for reqs in nodetemplate.requirements:
for (k,v) in reqs.items():
name = v["node"]
if (name in nodetemplates_by_name):
nodetemplate.dependencies.append(nodetemplates_by_name[name])
nodetemplate.dependencies_names.append(name)

# go another level deep, as our requirements can have requirements...
for sd_req in v.get("requirements",[]):
for (sd_req_k, sd_req_v) in sd_req.items():
name = sd_req_v["node"]
if (name in nodetemplates_by_name):
nodetemplate.dependencies.append(nodetemplates_by_name[name])
nodetemplate.dependencies_names.append(name)

Line 59:self.template.nodetemplates 是目前使用到的 Template 的集合,在此是根據 tosca-parser/tosca_template.py 內部所定義的類別屬性而得(ToscaTemplate 類別將會利用 yaml 套件將整個 yaml 檔的屬性萃取出來,所以可以使用 self.template.nodetemplates 取得所有 template)。

Line 60:nodetemplates_by_name 是一個 dictionary,以 key-value pair 的形式來保存 Template 物件與名字的對應關係

而在之後的 for nodetemplate in self.template.nodetemplates: 將會於 self.template.nodetemplates List 當中的每一個 NodeTemplate 建立 list:dependencieslist:dependencies_names,並根據此 nodetemplaterequirements 去把他的相依模板加入到剛剛 New 的兩個 List:dependencies, dependencies_names

但是可能相依模板還有它的相依模板,因此這裡又多了一層 for loop 去把深一層相依模板讀取而出,以關係說明的話則是如下:

1
2
3
* A
* B
* C

A 這個 NodeTemplate include 了 B,而 B 也 include 了 C。
因此我們會將 A 的 dependenciesdependencies_names 寫上:B 與 C。

但是這種寫法會有一個問題:

1
2
3
4
* A
* B
* C
* D

如果 C 還有他的 dependency nodetemplate D,D 就會因此而沒有被讀取到,導致 A -> B -> C -> D 的相依關係因為缺少了 D 而出問題。

topsort_dependencies

剛剛程式已經將拓樸的相依關係都找出來了,但是我們的 NodeTemplate A 與 B 可能都需要使用到 NodeTemplate C,在這種情形如果以 for loop 去將每一個 NodeTemplate 的 dependencies 都引入,會導致重複 include 的問題(雖然我還不清楚如果這麼作會發生什麼問題),因此我們需要排序出一個 Include 的順序,讓我們將最深層的 NodeTemplate 先引入,引入的順序應該是:C -> A -> B

而以下程式碼略過參數定義及套件集合預先處理的步驟,在此將直接以文字說明之。

首先我們會把 nodetemplates_by_name 指派到一個 g 變數,剛剛有提到 nodetemplates_by_name 是一個 dict,把名字與 object instance 以 key-value pair 的方式存起來,所以我們把 key(NodeTemplate 的 Name)作為一個集合,並製作一個叫做 value 的空集合。

這一個 value 的空集合將會去把所有 g.value() 的 dependencies_names 讀出並以 OR operation 與 value set 作運算(也就是這邊的相依模板名稱不會重複 - Set 的特性)。

在製作以下幾個變數:

  • all_node: key 和 value 兩個集合的內容物做成的陣列
  • order: 空陣列
  • stack: 空陣列
  • unmark: 還沒被排入 order 的物件,所以是 unmark = all_node

排序法程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
while unmarked:
stack.insert(0,unmarked[0]) # push first unmarked

while (stack):
n = stack[0]
add = True
try:
for m in g[n].dependencies_names:
if (m in unmarked):
add = False
stack.insert(0,m)
except KeyError:
pass
if (add):
if (n in steps and n not in order):
order.append(n)
item = stack.pop(0)
try:
unmarked.remove(item)
except ValueError:
pass

noorder = list(set(steps) - set(order))
return order + noorder

只要在 unmarked List 裡面還有東西時,我們就會把第一個物件 $n$ 放到 stack 裡面,且做了一個 add flag,只要為 True 就會被放到 order list。

如果 $n$ 的相依套件在 unmark 當中,那就把 add 設定為 False,並把他的相依套件放到 stack 頂端。

如果 add 為 True,就會將 $n$ 排入 order list(因為他沒有任何相依套件,所以最先被放進去是當然的),並且把 stack 頂端的物件 pop 出(頂端一定會是 $n$),再把 $n$ 從 unmarked list 移出。

這一段有冗贅程式碼,像是 unmark list 與 step 用途相同,不需要再作一個 step 來混淆邏輯

最後排序出來的 order 是一個相依關係從零到高的排序,而最後當然也會有一些沒有被排進 order 的物件,就把他們按照順序合併在一起回傳了。

XOSTosca.execute()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def execute(self, user):
for nodetemplate in self.ordered_nodetemplates:
self.execute_nodetemplate(user, nodetemplate)

for obj in self.deferred_sync:
self.log("Saving deferred sync obj %s" % obj)
obj.no_sync = False
obj.save()

def execute_nodetemplate(self, user, nodetemplate):
if nodetemplate.type not in resources.resources:
raise Exception("Nodetemplate %s's type %s is not a known resource" % (nodetemplate.name, nodetemplate.type))

cls = resources.resources[nodetemplate.type]
#print "work on", cls.__name__, nodetemplate.name
obj = cls(user, nodetemplate, self)
obj.create_or_update()
self.deferred_sync = self.deferred_sync + obj.deferred_sync

這邊所作僅僅是將剛剛的 NodeTemplate 做出 instance 而已。

但是這邊的 resources.resources 我還搞不懂是從哪裡來的,不確定是不是 xosresource.py

回歸到 MakeFile 裡面應該要有的內容(ref: service-profile/cord-pod/MakeFile)

以下內容也就是一開始張貼的 MakeFile 程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
exampleservice: $(SERVICE_DIR)/exampleservice
@echo "[EXAMPLESERVICE]"
sudo cp id_rsa key_import/exampleservice_rsa
sudo cp id_rsa.pub key_import/exampleservice_rsa.pub
$(RUN_TOSCA_BOOTSTRAP) $(COMMON_DIR)/tosca/disable-onboarding.yaml
$(RUN_TOSCA_BOOTSTRAP) $(SERVICE_DIR)/exampleservice/xos/exampleservice-onboard.yaml
$(RUN_TOSCA_BOOTSTRAP) exampleservice-synchronizer.yaml
$(RUN_TOSCA_BOOTSTRAP) $(COMMON_DIR)/tosca/enable-onboarding.yaml
bash $(COMMON_DIR)/wait_for_onboarding_ready.sh $(XOS_BOOTSTRAP_PORT) services/exampleservice
bash $(COMMON_DIR)/wait_for_onboarding_ready.sh $(XOS_BOOTSTRAP_PORT) xos
bash $(COMMON_DIR)/wait_for_xos_port.sh $(XOS_UI_PORT)
$(RUN_TOSCA) exampleservice.yaml
sleep 60
  • Copy Public/Private Key
  • Input yaml file to TOSCA engine
    1. disable onboarding
      • no-create: Do not allow TOSCA to create this object
      • no-delete: Do not allow TOSCA to delete this object
      • enable_build: if False means disable TOSCA build
    2. exampleservice_onboard
    3. exampleservice_synchronizer
      • 裡面會定義 exampleservice_config(db 帳號密碼,ip port … 等資訊)
    4. enable onboarding
  • waiting for onboarding

後記

這篇紀錄裡面沒有帶到 tosca-parser 之中定義的 TopologyTemplate, NodeTemplate 等,因為只是型別定義我也就沒有講述了。

如有興趣可以參考以下幾個連結: