Enable black in tox.ini
[osm/PLA.git] / osm_pla / placement / mznplacement.py
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.
15 import datetime
16 import platform
17 import itertools
18
19 import pymzn
20 from jinja2 import Environment
21 from jinja2.loaders import FileSystemLoader, PackageLoader, ChoiceLoader
22
23
24 class 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
96 class 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]))
135 return env.get_template(template_name)
136
137
138 class 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