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 |
1 |
if platform.system() == 'Windows': |
29 |
0 |
default_mzn_path = 'C:\\Program Files\\MiniZinc IDE (bundled)\\minizinc.exe' |
30 |
|
else: |
31 |
1 |
default_mzn_path = '/minizinc/bin/minizinc' |
32 |
|
|
33 |
1 |
def __init__(self, log, mzn_path=default_mzn_path): |
34 |
1 |
pymzn.config['minizinc'] = mzn_path |
35 |
1 |
self.log = log # FIXME what to log (besides forwarding it to MznModelGenerator) here? |
36 |
|
|
37 |
1 |
def _run_placement_model(self, mzn_model, ns_desc, mzn_model_data={}): |
38 |
|
""" |
39 |
|
Runs the minizinc placement model and post process the result |
40 |
|
Note: in this revision we use the 'item' output mode from pymzn.minizinc since it ease |
41 |
|
post processing of the solutions when we use enumerations in mzn_model |
42 |
|
Note: minizinc does not support '-' in identifiers and therefore we convert back from use of '_' when we |
43 |
|
process the result |
44 |
|
Note: minizinc does not support identifiers starting with numbers and therefore we skip the leading 'vim_' |
45 |
|
when we process the result |
46 |
|
|
47 |
|
:param mzn_model: a minizinc model as str (note: may also be path to .mzn file) |
48 |
|
:param ns_desc: network service descriptor, carries information about pinned VNFs so those can be included in |
49 |
|
the result |
50 |
|
:param mzn_model_data: minizinc model data dictionary (typically not used with our models) |
51 |
|
:return: list of dicts formatted as {'vimAccountId': '<account id>', 'member-vnf-index': <'index'>} |
52 |
|
or formatted as [{}] if unsatisfiable model |
53 |
|
""" |
54 |
1 |
solns = pymzn.minizinc(mzn_model, data=mzn_model_data, output_mode='item') |
55 |
|
|
56 |
1 |
if 'UNSATISFIABLE' in str(solns): |
57 |
1 |
return [{}] |
58 |
|
|
59 |
1 |
solns_as_str = str(solns[0]) |
60 |
|
|
61 |
|
# make it easier to extract the desired information by cleaning from newline, whitespace etc. |
62 |
1 |
solns_as_str = solns_as_str.replace('\n', '').replace(' ', '').rstrip(';') |
63 |
|
|
64 |
1 |
vnf_vim_mapping = (e.split('=') for e in solns_as_str.split(';')) |
65 |
|
|
66 |
1 |
res = [{'vimAccountId': e[1][3:].replace('_', '-'), 'member-vnf-index': e[0][3:]} for e in |
67 |
|
vnf_vim_mapping] |
68 |
|
# add any pinned VNFs |
69 |
1 |
pinned = [{'vimAccountId': e['vim_account'][3:].replace('_', '-'), 'member-vnf-index': e['vnf_id']} for e in |
70 |
|
ns_desc if 'vim_account' in e.keys()] |
71 |
|
|
72 |
1 |
return res + pinned |
73 |
|
|
74 |
1 |
def do_placement_computation(self, nspd): |
75 |
|
""" |
76 |
|
Orchestrates the placement computation |
77 |
|
|
78 |
|
:param nspd: placement data |
79 |
|
:return: see _run_placement_model |
80 |
|
""" |
81 |
1 |
mzn_model = MznModelGenerator(self.log).create_model(nspd) |
82 |
1 |
return self._run_placement_model(mzn_model, nspd['ns_desc']) |
83 |
|
|
84 |
|
|
85 |
1 |
class MznModelGenerator(object): |
86 |
|
''' |
87 |
|
Has the capability to generate minizinc models from information contained in |
88 |
|
NsPlacementData objects. Uses jinja2 as templating language for the model |
89 |
|
''' |
90 |
1 |
default_j2_template = "osm_pla_dynamic_template.j2" |
91 |
1 |
template_search_path = ['osm_pla/placement', '../placement', '/pla/osm_pla/placement', |
92 |
|
'./', '/usr/lib/python3/dist-packages/osm_pla/placement'] |
93 |
|
|
94 |
1 |
def __init__(self, log): |
95 |
|
''' |
96 |
|
Constructor |
97 |
|
''' |
98 |
1 |
self.log = log # FIXME we do not log anything so far |
99 |
|
|
100 |
1 |
def create_model(self, ns_placement_data): |
101 |
|
''' |
102 |
|
Creates a minizinc model according to the content of nspd |
103 |
|
nspd - NSPlacementData |
104 |
|
return MZNModel |
105 |
|
''' |
106 |
1 |
self.log.info('ns_desc: {}'.format(ns_placement_data['ns_desc'])) |
107 |
1 |
self.log.info('vld_desc: {}'.format(ns_placement_data['vld_desc'])) |
108 |
1 |
mzn_model_template = self._load_jinja_template() |
109 |
1 |
mzn_model = mzn_model_template.render(ns_placement_data) |
110 |
1 |
self.log.info('Minizinc model: {}'.format(mzn_model)) |
111 |
1 |
return mzn_model |
112 |
|
|
113 |
1 |
def _load_jinja_template(self, template_name=default_j2_template): |
114 |
|
"""loads the jinja template used for model generation""" |
115 |
1 |
loader1 = FileSystemLoader(MznModelGenerator.template_search_path) |
116 |
1 |
loader2 = PackageLoader('osm_pla', '.') |
117 |
1 |
env = Environment(loader=ChoiceLoader([loader1, loader2])) |
118 |
1 |
return env.get_template(template_name) |
119 |
|
|
120 |
|
|
121 |
1 |
class NsPlacementDataFactory(object): |
122 |
|
""" |
123 |
|
process information an network service and applicable network infrastructure resources in order to produce |
124 |
|
information tailored for the minizinc model code generator |
125 |
|
""" |
126 |
|
|
127 |
1 |
def __init__(self, vim_accounts_info, vnf_prices, nsd, pil_info, pinning=None, order_constraints=None): |
128 |
|
""" |
129 |
|
:param vim_accounts_info: a dictionary with vim url as key and id as value, we add a unique index to it for use |
130 |
|
in the mzn array constructs and adjust the value of the id to minizinc acceptable identifier syntax |
131 |
|
:param vnf_prices: a dictionary with 'vnfd-id-ref' as key and a dictionary with vim_urls: cost as value |
132 |
|
:param nsd: the network service descriptor |
133 |
|
:param pil_info: price list and metrics for PoP interconnection links |
134 |
|
:param pinning: list of {'member-vnf-index': '<idx>', 'vim_account': '<vim-account>'} |
135 |
|
:param order_constraints: any constraints provided at instantiation time |
136 |
|
""" |
137 |
1 |
next_idx = itertools.count() |
138 |
1 |
self._vim_accounts_info = {k: {'id': 'vim' + v.replace('-', '_'), 'idx': next(next_idx)} for k, v in |
139 |
|
vim_accounts_info.items()} |
140 |
1 |
self._vnf_prices = vnf_prices |
141 |
1 |
self._nsd = nsd |
142 |
1 |
self._pil_info = pil_info |
143 |
1 |
self._pinning = pinning |
144 |
1 |
self._order_constraints = order_constraints |
145 |
|
|
146 |
1 |
def _produce_trp_link_characteristics_data(self, characteristics): |
147 |
|
""" |
148 |
|
:param characteristics: one of {pil_latency, pil_price, pil_jitter} |
149 |
|
:return: 2d array of requested trp_link characteristics data |
150 |
|
""" |
151 |
1 |
if characteristics not in {'pil_latency', 'pil_price', 'pil_jitter'}: |
152 |
1 |
raise Exception('characteristic \'{}\' not supported'.format(characteristics)) |
153 |
1 |
num_vims = len(self._vim_accounts_info) |
154 |
1 |
trp_link_characteristics = [[0 if col == row else 0x7fff for col in range(num_vims)] for row in range(num_vims)] |
155 |
1 |
for pil in self._pil_info['pil']: |
156 |
1 |
if characteristics in pil.keys(): |
157 |
1 |
ep1 = pil['pil_endpoints'][0] |
158 |
1 |
ep2 = pil['pil_endpoints'][1] |
159 |
|
# only consider links between applicable vims |
160 |
1 |
if ep1 in self._vim_accounts_info and ep2 in self._vim_accounts_info: |
161 |
1 |
idx1 = self._vim_accounts_info[ep1]['idx'] |
162 |
1 |
idx2 = self._vim_accounts_info[ep2]['idx'] |
163 |
1 |
trp_link_characteristics[idx1][idx2] = pil[characteristics] |
164 |
1 |
trp_link_characteristics[idx2][idx1] = pil[characteristics] |
165 |
|
|
166 |
1 |
return trp_link_characteristics |
167 |
|
|
168 |
|
# TODO: Review if we should adapt this method with SOL006 /nsd/vnfd/int-virtual-link-desc/df/qos fields. |
169 |
1 |
def _produce_vld_desc(self): |
170 |
|
""" |
171 |
|
Creates the expected vlds from the nsd. Includes constraints if part of nsd. |
172 |
|
Overrides constraints with any syntactically correct instantiation parameters |
173 |
|
:return: |
174 |
|
""" |
175 |
|
|
176 |
1 |
all_vld_member_vnf_index_refs = {} |
177 |
|
# TODO: Change for multiple DF support |
178 |
1 |
ns_df = self._nsd.get('df', [{}])[0] |
179 |
1 |
for vnf_profile in ns_df.get('vnf-profile', []): |
180 |
1 |
for vlc in vnf_profile.get('virtual-link-connectivity', []): |
181 |
1 |
vld_id = vlc.get('virtual-link-profile-id') |
182 |
1 |
vld_member_vnf_index_ref = vnf_profile.get('id') |
183 |
1 |
if vld_id in all_vld_member_vnf_index_refs: |
184 |
1 |
all_vld_member_vnf_index_refs[vld_id].append(vld_member_vnf_index_ref) |
185 |
|
else: |
186 |
1 |
all_vld_member_vnf_index_refs[vld_id] = [vld_member_vnf_index_ref] |
187 |
|
|
188 |
1 |
vld_desc = [] |
189 |
1 |
for vld in self._nsd.get('virtual-link-desc', ()): |
190 |
1 |
if vld.get('mgmt-network', False) is True: |
191 |
1 |
continue |
192 |
1 |
vld_desc_entry = {} |
193 |
1 |
cp_refs = all_vld_member_vnf_index_refs[vld.get('id')] |
194 |
1 |
if len(cp_refs) == 2: |
195 |
1 |
vld_desc_entry['cp_refs'] = cp_refs |
196 |
|
# TODO: Change for multiple DF support |
197 |
1 |
vld_df = vld.get('df', [{}])[0] |
198 |
1 |
for constraint in vld_df.get('qos', {}): |
199 |
1 |
if constraint == 'latency': |
200 |
1 |
vld_desc_entry['latency'] = vld_df['qos'][constraint] |
201 |
1 |
elif constraint == 'packet-delay-variation': |
202 |
1 |
vld_desc_entry['jitter'] = vld_df['qos'][constraint] |
203 |
1 |
vld_desc.append(vld_desc_entry) |
204 |
|
|
205 |
|
# create candidates from instantiate params |
206 |
1 |
if self._order_constraints is not None: |
207 |
1 |
candidate_vld_desc = [] |
208 |
|
# use id to find the endpoints in the nsd |
209 |
1 |
for entry in self._order_constraints.get('vld-constraints'): |
210 |
1 |
for vld in self._nsd.get('virtual-link-desc', ()): |
211 |
1 |
if entry['id'] == vld.get('id'): |
212 |
1 |
vld_desc_instantiate_entry = {} |
213 |
1 |
cp_refs = all_vld_member_vnf_index_refs[vld.get('id')] |
214 |
1 |
vld_desc_instantiate_entry['cp_refs'] = cp_refs |
215 |
|
# add whatever constraints that are provided to the vld_desc_entry |
216 |
|
# misspelled 'link-constraints' => empty dict |
217 |
|
# lack (or misspelling) of one or both supported constraints => entry not appended |
218 |
1 |
for constraint, value in entry.get('link-constraints', {}).items(): |
219 |
1 |
if constraint == 'latency': |
220 |
1 |
vld_desc_instantiate_entry['latency'] = value |
221 |
1 |
elif constraint == 'jitter': |
222 |
1 |
vld_desc_instantiate_entry['jitter'] = value |
223 |
1 |
if {'latency', 'jitter'}.intersection(vld_desc_instantiate_entry.keys()): |
224 |
1 |
candidate_vld_desc.append(vld_desc_instantiate_entry) |
225 |
|
# merge with nsd originated, FIXME log any deviations? |
226 |
1 |
for vld_d in vld_desc: |
227 |
1 |
for vld_d_i in candidate_vld_desc: |
228 |
1 |
if set(vld_d['cp_refs']) == set(vld_d_i['cp_refs']): |
229 |
1 |
if vld_d_i.get('jitter'): |
230 |
1 |
vld_d['jitter'] = vld_d_i['jitter'] |
231 |
1 |
if vld_d_i.get('latency'): |
232 |
1 |
vld_d['latency'] = vld_d_i['latency'] |
233 |
|
|
234 |
1 |
return vld_desc |
235 |
|
|
236 |
1 |
def _produce_ns_desc(self): |
237 |
|
""" |
238 |
|
collect information for the ns_desc part of the placement data |
239 |
|
for the vim_accounts that are applicable, collect the vnf_price |
240 |
|
""" |
241 |
1 |
ns_desc = [] |
242 |
|
# TODO: Change for multiple DF support |
243 |
1 |
ns_df = self._nsd.get('df', [{}])[0] |
244 |
1 |
for vnf_profile in ns_df.get('vnf-profile', []): |
245 |
1 |
vnf_info = {'vnf_id': vnf_profile['id']} |
246 |
|
# prices |
247 |
1 |
prices_for_vnfd = self._vnf_prices[vnf_profile['vnfd-id']] |
248 |
|
# the list of prices must be ordered according to the indexing of the vim_accounts |
249 |
1 |
price_list = [_ for _ in range(len(self._vim_accounts_info))] |
250 |
1 |
for k in prices_for_vnfd.keys(): |
251 |
1 |
if k in self._vim_accounts_info.keys(): |
252 |
1 |
price_list[self._vim_accounts_info[k]['idx']] = prices_for_vnfd[k] |
253 |
1 |
vnf_info['vnf_price_per_vim'] = price_list |
254 |
|
|
255 |
|
# pinning to dc |
256 |
1 |
if self._pinning is not None: |
257 |
1 |
for pinned_vnf in self._pinning: |
258 |
1 |
if vnf_profile['id'] == pinned_vnf['member-vnf-index']: |
259 |
1 |
vnf_info['vim_account'] = 'vim' + pinned_vnf['vimAccountId'].replace('-', '_') |
260 |
|
|
261 |
1 |
ns_desc.append(vnf_info) |
262 |
1 |
return ns_desc |
263 |
|
|
264 |
1 |
def create_ns_placement_data(self): |
265 |
|
"""populate NsPlacmentData object |
266 |
|
""" |
267 |
1 |
ns_placement_data = {'vim_accounts': [vim_data['id'] for _, vim_data in sorted(self._vim_accounts_info.items(), |
268 |
|
key=lambda item: item[1][ |
269 |
|
'idx'])], |
270 |
|
'trp_link_latency': self._produce_trp_link_characteristics_data('pil_latency'), |
271 |
|
'trp_link_jitter': self._produce_trp_link_characteristics_data('pil_jitter'), |
272 |
|
'trp_link_price_list': self._produce_trp_link_characteristics_data('pil_price'), |
273 |
|
'ns_desc': self._produce_ns_desc(), |
274 |
|
'vld_desc': self._produce_vld_desc(), |
275 |
|
'generator_data': {'file': __file__, 'time': datetime.datetime.now()}} |
276 |
|
|
277 |
1 |
return ns_placement_data |