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 |