Code Coverage

Cobertura Coverage Report > osm_pla.placement >

mznplacement.py

Trend

File Coverage summary

NameClassesLinesConditionals
mznplacement.py
100%
1/1
99%
134/135
100%
0/0

Coverage Breakdown by Class

NameLinesConditionals
mznplacement.py
99%
134/135
N/A

Source

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 1 import datetime
16 1 import platform
17 1 import itertools
18
19 1 import pymzn
20 1 from jinja2 import Environment
21 1 from jinja2.loaders import FileSystemLoader, PackageLoader, ChoiceLoader
22
23
24 1 class MznPlacementConductor(object):
25     """
26     Knows how to process placement req using minizinc
27     """
28
29 1     if platform.system() == "Windows":
30 0         default_mzn_path = "C:\\Program Files\\MiniZinc IDE (bundled)\\minizinc.exe"
31     else:
32 1         default_mzn_path = "/minizinc/bin/minizinc"
33
34 1     def __init__(self, log, mzn_path=default_mzn_path):
35 1         pymzn.config["minizinc"] = mzn_path
36 1         self.log = (
37             log  # FIXME what to log (besides forwarding it to MznModelGenerator) here?
38         )
39
40 1     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 1         solns = pymzn.minizinc(mzn_model, data=mzn_model_data, output_mode="item")
58
59 1         if "UNSATISFIABLE" in str(solns):
60 1             return [{}]
61
62 1         solns_as_str = str(solns[0])
63
64         # make it easier to extract the desired information by cleaning from newline, whitespace etc.
65 1         solns_as_str = solns_as_str.replace("\n", "").replace(" ", "").rstrip(";")
66
67 1         vnf_vim_mapping = (e.split("=") for e in solns_as_str.split(";"))
68
69 1         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 1         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 1         return res + pinned
84
85 1     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 1         mzn_model = MznModelGenerator(self.log).create_model(nspd)
93 1         return self._run_placement_model(mzn_model, nspd["ns_desc"])
94
95
96 1 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 1     default_j2_template = "osm_pla_dynamic_template.j2"
103 1     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 1     def __init__(self, log):
112         """
113         Constructor
114         """
115 1         self.log = log  # FIXME we do not log anything so far
116
117 1     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 1         self.log.info("ns_desc: {}".format(ns_placement_data["ns_desc"]))
124 1         self.log.info("vld_desc: {}".format(ns_placement_data["vld_desc"]))
125 1         mzn_model_template = self._load_jinja_template()
126 1         mzn_model = mzn_model_template.render(ns_placement_data)
127 1         self.log.info("Minizinc model: {}".format(mzn_model))
128 1         return mzn_model
129
130 1     def _load_jinja_template(self, template_name=default_j2_template):
131         """loads the jinja template used for model generation"""
132 1         loader1 = FileSystemLoader(MznModelGenerator.template_search_path)
133 1         loader2 = PackageLoader("osm_pla", ".")
134 1         env = Environment(loader=ChoiceLoader([loader1, loader2]), autoescape=True)
135 1         return env.get_template(template_name)
136
137
138 1 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 1     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 1         next_idx = itertools.count()
163 1         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 1         self._vnf_prices = vnf_prices
168 1         self._nsd = nsd
169 1         self._pil_info = pil_info
170 1         self._pinning = pinning
171 1         self._order_constraints = order_constraints
172
173 1     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 1         if characteristics not in {"pil_latency", "pil_price", "pil_jitter"}:
179 1             raise Exception("characteristic '{}' not supported".format(characteristics))
180 1         num_vims = len(self._vim_accounts_info)
181 1         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 1         for pil in self._pil_info["pil"]:
186 1             if characteristics in pil.keys():
187 1                 ep1 = pil["pil_endpoints"][0]
188 1                 ep2 = pil["pil_endpoints"][1]
189                 # only consider links between applicable vims
190 1                 if ep1 in self._vim_accounts_info and ep2 in self._vim_accounts_info:
191 1                     idx1 = self._vim_accounts_info[ep1]["idx"]
192 1                     idx2 = self._vim_accounts_info[ep2]["idx"]
193 1                     trp_link_characteristics[idx1][idx2] = pil[characteristics]
194 1                     trp_link_characteristics[idx2][idx1] = pil[characteristics]
195
196 1         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 1     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 1         all_vld_member_vnf_index_refs = {}
207         # TODO: Change for multiple DF support
208 1         ns_df = self._nsd.get("df", [{}])[0]
209 1         for vnf_profile in ns_df.get("vnf-profile", []):
210 1             for vlc in vnf_profile.get("virtual-link-connectivity", []):
211 1                 vld_id = vlc.get("virtual-link-profile-id")
212 1                 vld_member_vnf_index_ref = vnf_profile.get("id")
213 1                 if vld_id in all_vld_member_vnf_index_refs:
214 1                     all_vld_member_vnf_index_refs[vld_id].append(
215                         vld_member_vnf_index_ref
216                     )
217                 else:
218 1                     all_vld_member_vnf_index_refs[vld_id] = [vld_member_vnf_index_ref]
219
220 1         vld_desc = []
221 1         for vld in self._nsd.get("virtual-link-desc", ()):
222 1             if vld.get("mgmt-network", False) is True:
223 1                 continue
224 1             vld_desc_entry = {}
225 1             cp_refs = all_vld_member_vnf_index_refs[vld.get("id")]
226 1             if len(cp_refs) == 2:
227 1                 vld_desc_entry["cp_refs"] = cp_refs
228                 # TODO: Change for multiple DF support
229 1                 vld_df = vld.get("df", [{}])[0]
230 1                 for constraint in vld_df.get("qos", {}):
231 1                     if constraint == "latency":
232 1                         vld_desc_entry["latency"] = vld_df["qos"][constraint]
233 1                     elif constraint == "packet-delay-variation":
234 1                         vld_desc_entry["jitter"] = vld_df["qos"][constraint]
235 1                 vld_desc.append(vld_desc_entry)
236
237         # create candidates from instantiate params
238 1         if self._order_constraints is not None:
239 1             candidate_vld_desc = []
240             # use id to find the endpoints in the nsd
241 1             for entry in self._order_constraints.get("vld-constraints"):
242 1                 for vld in self._nsd.get("virtual-link-desc", ()):
243 1                     if entry["id"] == vld.get("id"):
244 1                         vld_desc_instantiate_entry = {}
245 1                         cp_refs = all_vld_member_vnf_index_refs[vld.get("id")]
246 1                         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 1                         for constraint, value in entry.get(
251                             "link-constraints", {}
252                         ).items():
253 1                             if constraint == "latency":
254 1                                 vld_desc_instantiate_entry["latency"] = value
255 1                             elif constraint == "jitter":
256 1                                 vld_desc_instantiate_entry["jitter"] = value
257 1                         if {"latency", "jitter"}.intersection(
258                             vld_desc_instantiate_entry.keys()
259                         ):
260 1                             candidate_vld_desc.append(vld_desc_instantiate_entry)
261             # merge with nsd originated, FIXME log any deviations?
262 1             for vld_d in vld_desc:
263 1                 for vld_d_i in candidate_vld_desc:
264 1                     if set(vld_d["cp_refs"]) == set(vld_d_i["cp_refs"]):
265 1                         if vld_d_i.get("jitter"):
266 1                             vld_d["jitter"] = vld_d_i["jitter"]
267 1                         if vld_d_i.get("latency"):
268 1                             vld_d["latency"] = vld_d_i["latency"]
269
270 1         return vld_desc
271
272 1     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 1         ns_desc = []
278         # TODO: Change for multiple DF support
279 1         ns_df = self._nsd.get("df", [{}])[0]
280 1         for vnf_profile in ns_df.get("vnf-profile", []):
281 1             vnf_info = {"vnf_id": vnf_profile["id"]}
282             # prices
283 1             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 1             price_list = [_ for _ in range(len(self._vim_accounts_info))]
286 1             for k in prices_for_vnfd.keys():
287 1                 if k in self._vim_accounts_info.keys():
288 1                     price_list[self._vim_accounts_info[k]["idx"]] = prices_for_vnfd[k]
289 1             vnf_info["vnf_price_per_vim"] = price_list
290
291             # pinning to dc
292 1             if self._pinning is not None:
293 1                 for pinned_vnf in self._pinning:
294 1                     if vnf_profile["id"] == pinned_vnf["member-vnf-index"]:
295 1                         vnf_info["vim_account"] = "vim" + pinned_vnf[
296                             "vimAccountId"
297                         ].replace("-", "_")
298
299 1             ns_desc.append(vnf_info)
300 1         return ns_desc
301
302 1     def create_ns_placement_data(self):
303         """populate NsPlacmentData object"""
304 1         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 1         return ns_placement_data