Coverage for osm_pla/placement/mznplacement.py: 99%

135 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2024-06-22 10:12 +0000

1# Copyright 2020 ArctosLabs Scandinavia AB 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 

12# implied. 

13# See the License for the specific language governing permissions and 

14# limitations under the License. 

15import datetime 

16import platform 

17import itertools 

18 

19import pymzn 

20from jinja2 import Environment 

21from jinja2.loaders import FileSystemLoader, PackageLoader, ChoiceLoader 

22 

23 

24class MznPlacementConductor(object): 

25 """ 

26 Knows how to process placement req using minizinc 

27 """ 

28 

29 if platform.system() == "Windows": 

30 default_mzn_path = "C:\\Program Files\\MiniZinc IDE (bundled)\\minizinc.exe" 

31 else: 

32 default_mzn_path = "/minizinc/bin/minizinc" 

33 

34 def __init__(self, log, mzn_path=default_mzn_path): 

35 pymzn.config["minizinc"] = mzn_path 

36 self.log = ( 

37 log # FIXME what to log (besides forwarding it to MznModelGenerator) here? 

38 ) 

39 

40 def _run_placement_model(self, mzn_model, ns_desc, mzn_model_data={}): 

41 """ 

42 Runs the minizinc placement model and post process the result 

43 Note: in this revision we use the 'item' output mode from pymzn.minizinc since it ease 

44 post processing of the solutions when we use enumerations in mzn_model 

45 Note: minizinc does not support '-' in identifiers and therefore we convert back from use of '_' when we 

46 process the result 

47 Note: minizinc does not support identifiers starting with numbers and therefore we skip the leading 'vim_' 

48 when we process the result 

49 

50 :param mzn_model: a minizinc model as str (note: may also be path to .mzn file) 

51 :param ns_desc: network service descriptor, carries information about pinned VNFs so those can be included in 

52 the result 

53 :param mzn_model_data: minizinc model data dictionary (typically not used with our models) 

54 :return: list of dicts formatted as {'vimAccountId': '<account id>', 'member-vnf-index': <'index'>} 

55 or formatted as [{}] if unsatisfiable model 

56 """ 

57 solns = pymzn.minizinc(mzn_model, data=mzn_model_data, output_mode="item") 

58 

59 if "UNSATISFIABLE" in str(solns): 

60 return [{}] 

61 

62 solns_as_str = str(solns[0]) 

63 

64 # make it easier to extract the desired information by cleaning from newline, whitespace etc. 

65 solns_as_str = solns_as_str.replace("\n", "").replace(" ", "").rstrip(";") 

66 

67 vnf_vim_mapping = (e.split("=") for e in solns_as_str.split(";")) 

68 

69 res = [ 

70 {"vimAccountId": e[1][3:].replace("_", "-"), "member-vnf-index": e[0][3:]} 

71 for e in vnf_vim_mapping 

72 ] 

73 # add any pinned VNFs 

74 pinned = [ 

75 { 

76 "vimAccountId": e["vim_account"][3:].replace("_", "-"), 

77 "member-vnf-index": e["vnf_id"], 

78 } 

79 for e in ns_desc 

80 if "vim_account" in e.keys() 

81 ] 

82 

83 return res + pinned 

84 

85 def do_placement_computation(self, nspd): 

86 """ 

87 Orchestrates the placement computation 

88 

89 :param nspd: placement data 

90 :return: see _run_placement_model 

91 """ 

92 mzn_model = MznModelGenerator(self.log).create_model(nspd) 

93 return self._run_placement_model(mzn_model, nspd["ns_desc"]) 

94 

95 

96class MznModelGenerator(object): 

97 """ 

98 Has the capability to generate minizinc models from information contained in 

99 NsPlacementData objects. Uses jinja2 as templating language for the model 

100 """ 

101 

102 default_j2_template = "osm_pla_dynamic_template.j2" 

103 template_search_path = [ 

104 "osm_pla/placement", 

105 "../placement", 

106 "/pla/osm_pla/placement", 

107 "./", 

108 "/usr/lib/python3/dist-packages/osm_pla/placement", 

109 ] 

110 

111 def __init__(self, log): 

112 """ 

113 Constructor 

114 """ 

115 self.log = log # FIXME we do not log anything so far 

116 

117 def create_model(self, ns_placement_data): 

118 """ 

119 Creates a minizinc model according to the content of nspd 

120 nspd - NSPlacementData 

121 return MZNModel 

122 """ 

123 self.log.info("ns_desc: {}".format(ns_placement_data["ns_desc"])) 

124 self.log.info("vld_desc: {}".format(ns_placement_data["vld_desc"])) 

125 mzn_model_template = self._load_jinja_template() 

126 mzn_model = mzn_model_template.render(ns_placement_data) 

127 self.log.info("Minizinc model: {}".format(mzn_model)) 

128 return mzn_model 

129 

130 def _load_jinja_template(self, template_name=default_j2_template): 

131 """loads the jinja template used for model generation""" 

132 loader1 = FileSystemLoader(MznModelGenerator.template_search_path) 

133 loader2 = PackageLoader("osm_pla", ".") 

134 env = Environment(loader=ChoiceLoader([loader1, loader2]), autoescape=True) 

135 return env.get_template(template_name) 

136 

137 

138class NsPlacementDataFactory(object): 

139 """ 

140 process information an network service and applicable network infrastructure resources in order to produce 

141 information tailored for the minizinc model code generator 

142 """ 

143 

144 def __init__( 

145 self, 

146 vim_accounts_info, 

147 vnf_prices, 

148 nsd, 

149 pil_info, 

150 pinning=None, 

151 order_constraints=None, 

152 ): 

153 """ 

154 :param vim_accounts_info: a dictionary with vim url as key and id as value, we add a unique index to it for use 

155 in the mzn array constructs and adjust the value of the id to minizinc acceptable identifier syntax 

156 :param vnf_prices: a dictionary with 'vnfd-id-ref' as key and a dictionary with vim_urls: cost as value 

157 :param nsd: the network service descriptor 

158 :param pil_info: price list and metrics for PoP interconnection links 

159 :param pinning: list of {'member-vnf-index': '<idx>', 'vim_account': '<vim-account>'} 

160 :param order_constraints: any constraints provided at instantiation time 

161 """ 

162 next_idx = itertools.count() 

163 self._vim_accounts_info = { 

164 k: {"id": "vim" + v.replace("-", "_"), "idx": next(next_idx)} 

165 for k, v in vim_accounts_info.items() 

166 } 

167 self._vnf_prices = vnf_prices 

168 self._nsd = nsd 

169 self._pil_info = pil_info 

170 self._pinning = pinning 

171 self._order_constraints = order_constraints 

172 

173 def _produce_trp_link_characteristics_data(self, characteristics): 

174 """ 

175 :param characteristics: one of {pil_latency, pil_price, pil_jitter} 

176 :return: 2d array of requested trp_link characteristics data 

177 """ 

178 if characteristics not in {"pil_latency", "pil_price", "pil_jitter"}: 

179 raise Exception("characteristic '{}' not supported".format(characteristics)) 

180 num_vims = len(self._vim_accounts_info) 

181 trp_link_characteristics = [ 

182 [0 if col == row else 0x7FFF for col in range(num_vims)] 

183 for row in range(num_vims) 

184 ] 

185 for pil in self._pil_info["pil"]: 

186 if characteristics in pil.keys(): 

187 ep1 = pil["pil_endpoints"][0] 

188 ep2 = pil["pil_endpoints"][1] 

189 # only consider links between applicable vims 

190 if ep1 in self._vim_accounts_info and ep2 in self._vim_accounts_info: 

191 idx1 = self._vim_accounts_info[ep1]["idx"] 

192 idx2 = self._vim_accounts_info[ep2]["idx"] 

193 trp_link_characteristics[idx1][idx2] = pil[characteristics] 

194 trp_link_characteristics[idx2][idx1] = pil[characteristics] 

195 

196 return trp_link_characteristics 

197 

198 # TODO: Review if we should adapt this method with SOL006 /nsd/vnfd/int-virtual-link-desc/df/qos fields. 

199 def _produce_vld_desc(self): 

200 """ 

201 Creates the expected vlds from the nsd. Includes constraints if part of nsd. 

202 Overrides constraints with any syntactically correct instantiation parameters 

203 :return: 

204 """ 

205 

206 all_vld_member_vnf_index_refs = {} 

207 # TODO: Change for multiple DF support 

208 ns_df = self._nsd.get("df", [{}])[0] 

209 for vnf_profile in ns_df.get("vnf-profile", []): 

210 for vlc in vnf_profile.get("virtual-link-connectivity", []): 

211 vld_id = vlc.get("virtual-link-profile-id") 

212 vld_member_vnf_index_ref = vnf_profile.get("id") 

213 if vld_id in all_vld_member_vnf_index_refs: 

214 all_vld_member_vnf_index_refs[vld_id].append( 

215 vld_member_vnf_index_ref 

216 ) 

217 else: 

218 all_vld_member_vnf_index_refs[vld_id] = [vld_member_vnf_index_ref] 

219 

220 vld_desc = [] 

221 for vld in self._nsd.get("virtual-link-desc", ()): 

222 if vld.get("mgmt-network", False) is True: 

223 continue 

224 vld_desc_entry = {} 

225 cp_refs = all_vld_member_vnf_index_refs[vld.get("id")] 

226 if len(cp_refs) == 2: 

227 vld_desc_entry["cp_refs"] = cp_refs 

228 # TODO: Change for multiple DF support 

229 vld_df = vld.get("df", [{}])[0] 

230 for constraint in vld_df.get("qos", {}): 

231 if constraint == "latency": 

232 vld_desc_entry["latency"] = vld_df["qos"][constraint] 

233 elif constraint == "packet-delay-variation": 

234 vld_desc_entry["jitter"] = vld_df["qos"][constraint] 

235 vld_desc.append(vld_desc_entry) 

236 

237 # create candidates from instantiate params 

238 if self._order_constraints is not None: 

239 candidate_vld_desc = [] 

240 # use id to find the endpoints in the nsd 

241 for entry in self._order_constraints.get("vld-constraints"): 

242 for vld in self._nsd.get("virtual-link-desc", ()): 

243 if entry["id"] == vld.get("id"): 

244 vld_desc_instantiate_entry = {} 

245 cp_refs = all_vld_member_vnf_index_refs[vld.get("id")] 

246 vld_desc_instantiate_entry["cp_refs"] = cp_refs 

247 # add whatever constraints that are provided to the vld_desc_entry 

248 # misspelled 'link-constraints' => empty dict 

249 # lack (or misspelling) of one or both supported constraints => entry not appended 

250 for constraint, value in entry.get( 

251 "link-constraints", {} 

252 ).items(): 

253 if constraint == "latency": 

254 vld_desc_instantiate_entry["latency"] = value 

255 elif constraint == "jitter": 

256 vld_desc_instantiate_entry["jitter"] = value 

257 if {"latency", "jitter"}.intersection( 

258 vld_desc_instantiate_entry.keys() 

259 ): 

260 candidate_vld_desc.append(vld_desc_instantiate_entry) 

261 # merge with nsd originated, FIXME log any deviations? 

262 for vld_d in vld_desc: 

263 for vld_d_i in candidate_vld_desc: 

264 if set(vld_d["cp_refs"]) == set(vld_d_i["cp_refs"]): 

265 if vld_d_i.get("jitter"): 

266 vld_d["jitter"] = vld_d_i["jitter"] 

267 if vld_d_i.get("latency"): 

268 vld_d["latency"] = vld_d_i["latency"] 

269 

270 return vld_desc 

271 

272 def _produce_ns_desc(self): 

273 """ 

274 collect information for the ns_desc part of the placement data 

275 for the vim_accounts that are applicable, collect the vnf_price 

276 """ 

277 ns_desc = [] 

278 # TODO: Change for multiple DF support 

279 ns_df = self._nsd.get("df", [{}])[0] 

280 for vnf_profile in ns_df.get("vnf-profile", []): 

281 vnf_info = {"vnf_id": vnf_profile["id"]} 

282 # prices 

283 prices_for_vnfd = self._vnf_prices[vnf_profile["vnfd-id"]] 

284 # the list of prices must be ordered according to the indexing of the vim_accounts 

285 price_list = [_ for _ in range(len(self._vim_accounts_info))] 

286 for k in prices_for_vnfd.keys(): 

287 if k in self._vim_accounts_info.keys(): 

288 price_list[self._vim_accounts_info[k]["idx"]] = prices_for_vnfd[k] 

289 vnf_info["vnf_price_per_vim"] = price_list 

290 

291 # pinning to dc 

292 if self._pinning is not None: 

293 for pinned_vnf in self._pinning: 

294 if vnf_profile["id"] == pinned_vnf["member-vnf-index"]: 

295 vnf_info["vim_account"] = "vim" + pinned_vnf[ 

296 "vimAccountId" 

297 ].replace("-", "_") 

298 

299 ns_desc.append(vnf_info) 

300 return ns_desc 

301 

302 def create_ns_placement_data(self): 

303 """populate NsPlacmentData object""" 

304 ns_placement_data = { 

305 "vim_accounts": [ 

306 vim_data["id"] 

307 for _, vim_data in sorted( 

308 self._vim_accounts_info.items(), key=lambda item: item[1]["idx"] 

309 ) 

310 ], 

311 "trp_link_latency": self._produce_trp_link_characteristics_data( 

312 "pil_latency" 

313 ), 

314 "trp_link_jitter": self._produce_trp_link_characteristics_data( 

315 "pil_jitter" 

316 ), 

317 "trp_link_price_list": self._produce_trp_link_characteristics_data( 

318 "pil_price" 

319 ), 

320 "ns_desc": self._produce_ns_desc(), 

321 "vld_desc": self._produce_vld_desc(), 

322 "generator_data": {"file": __file__, "time": datetime.datetime.now()}, 

323 } 

324 

325 return ns_placement_data