Adapts PLA to new SOL006 NSD descriptors format
[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 if platform.system() == 'Windows':
29 default_mzn_path = 'C:\\Program Files\\MiniZinc IDE (bundled)\\minizinc.exe'
30 else:
31 default_mzn_path = '/minizinc/bin/minizinc'
32
33 def __init__(self, log, mzn_path=default_mzn_path):
34 pymzn.config['minizinc'] = mzn_path
35 self.log = log # FIXME what to log (besides forwarding it to MznModelGenerator) here?
36
37 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 solns = pymzn.minizinc(mzn_model, data=mzn_model_data, output_mode='item')
55
56 if 'UNSATISFIABLE' in str(solns):
57 return [{}]
58
59 solns_as_str = str(solns[0])
60
61 # make it easier to extract the desired information by cleaning from newline, whitespace etc.
62 solns_as_str = solns_as_str.replace('\n', '').replace(' ', '').rstrip(';')
63
64 vnf_vim_mapping = (e.split('=') for e in solns_as_str.split(';'))
65
66 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 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 return res + pinned
73
74 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 mzn_model = MznModelGenerator(self.log).create_model(nspd)
82 return self._run_placement_model(mzn_model, nspd['ns_desc'])
83
84
85 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 default_j2_template = "osm_pla_dynamic_template.j2"
91 template_search_path = ['osm_pla/placement', '../placement', '/pla/osm_pla/placement',
92 './', '/usr/lib/python3/dist-packages/osm_pla/placement']
93
94 def __init__(self, log):
95 '''
96 Constructor
97 '''
98 self.log = log # FIXME we do not log anything so far
99
100 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 self.log.info('ns_desc: {}'.format(ns_placement_data['ns_desc']))
107 self.log.info('vld_desc: {}'.format(ns_placement_data['vld_desc']))
108 mzn_model_template = self._load_jinja_template()
109 mzn_model = mzn_model_template.render(ns_placement_data)
110 self.log.info('Minizinc model: {}'.format(mzn_model))
111 return mzn_model
112
113 def _load_jinja_template(self, template_name=default_j2_template):
114 """loads the jinja template used for model generation"""
115 loader1 = FileSystemLoader(MznModelGenerator.template_search_path)
116 loader2 = PackageLoader('osm_pla', '.')
117 env = Environment(loader=ChoiceLoader([loader1, loader2]))
118 return env.get_template(template_name)
119
120
121 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 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 next_idx = itertools.count()
138 self._vim_accounts_info = {k: {'id': 'vim' + v.replace('-', '_'), 'idx': next(next_idx)} for k, v in
139 vim_accounts_info.items()}
140 self._vnf_prices = vnf_prices
141 self._nsd = nsd
142 self._pil_info = pil_info
143 self._pinning = pinning
144 self._order_constraints = order_constraints
145
146 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 if characteristics not in {'pil_latency', 'pil_price', 'pil_jitter'}:
152 raise Exception('characteristic \'{}\' not supported'.format(characteristics))
153 num_vims = len(self._vim_accounts_info)
154 trp_link_characteristics = [[0 if col == row else 0x7fff for col in range(num_vims)] for row in range(num_vims)]
155 for pil in self._pil_info['pil']:
156 if characteristics in pil.keys():
157 ep1 = pil['pil_endpoints'][0]
158 ep2 = pil['pil_endpoints'][1]
159 # only consider links between applicable vims
160 if ep1 in self._vim_accounts_info and ep2 in self._vim_accounts_info:
161 idx1 = self._vim_accounts_info[ep1]['idx']
162 idx2 = self._vim_accounts_info[ep2]['idx']
163 trp_link_characteristics[idx1][idx2] = pil[characteristics]
164 trp_link_characteristics[idx2][idx1] = pil[characteristics]
165
166 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 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 all_vld_member_vnf_index_refs = {}
177 # TODO: Change for multiple DF support
178 ns_df = self._nsd.get('df', [{}])[0]
179 for vnf_profile in ns_df.get('vnf-profile', []):
180 for vlc in vnf_profile.get('virtual-link-connectivity', []):
181 vld_id = vlc.get('virtual-link-profile-id')
182 vld_member_vnf_index_ref = vnf_profile.get('id')
183 if vld_id in all_vld_member_vnf_index_refs:
184 all_vld_member_vnf_index_refs[vld_id].append(vld_member_vnf_index_ref)
185 else:
186 all_vld_member_vnf_index_refs[vld_id] = [vld_member_vnf_index_ref]
187
188 vld_desc = []
189 for vld in self._nsd.get('virtual-link-desc', ()):
190 if vld.get('mgmt-network', False) is True:
191 continue
192 vld_desc_entry = {}
193 cp_refs = all_vld_member_vnf_index_refs[vld.get('id')]
194 if len(cp_refs) == 2:
195 vld_desc_entry['cp_refs'] = cp_refs
196 # TODO: Change for multiple DF support
197 vld_df = vld.get('df', [{}])[0]
198 for constraint in vld_df.get('qos', {}):
199 if constraint == 'latency':
200 vld_desc_entry['latency'] = vld_df['qos'][constraint]
201 elif constraint == 'packet-delay-variation':
202 vld_desc_entry['jitter'] = vld_df['qos'][constraint]
203 vld_desc.append(vld_desc_entry)
204
205 # create candidates from instantiate params
206 if self._order_constraints is not None:
207 candidate_vld_desc = []
208 # use id to find the endpoints in the nsd
209 for entry in self._order_constraints.get('vld-constraints'):
210 for vld in self._nsd.get('virtual-link-desc', ()):
211 if entry['id'] == vld.get('id'):
212 vld_desc_instantiate_entry = {}
213 cp_refs = all_vld_member_vnf_index_refs[vld.get('id')]
214 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 for constraint, value in entry.get('link-constraints', {}).items():
219 if constraint == 'latency':
220 vld_desc_instantiate_entry['latency'] = value
221 elif constraint == 'jitter':
222 vld_desc_instantiate_entry['jitter'] = value
223 if {'latency', 'jitter'}.intersection(vld_desc_instantiate_entry.keys()):
224 candidate_vld_desc.append(vld_desc_instantiate_entry)
225 # merge with nsd originated, FIXME log any deviations?
226 for vld_d in vld_desc:
227 for vld_d_i in candidate_vld_desc:
228 if set(vld_d['cp_refs']) == set(vld_d_i['cp_refs']):
229 if vld_d_i.get('jitter'):
230 vld_d['jitter'] = vld_d_i['jitter']
231 if vld_d_i.get('latency'):
232 vld_d['latency'] = vld_d_i['latency']
233
234 return vld_desc
235
236 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 ns_desc = []
242 # TODO: Change for multiple DF support
243 ns_df = self._nsd.get('df', [{}])[0]
244 for vnf_profile in ns_df.get('vnf-profile', []):
245 vnf_info = {'vnf_id': vnf_profile['id']}
246 # prices
247 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 price_list = [_ for _ in range(len(self._vim_accounts_info))]
250 for k in prices_for_vnfd.keys():
251 if k in self._vim_accounts_info.keys():
252 price_list[self._vim_accounts_info[k]['idx']] = prices_for_vnfd[k]
253 vnf_info['vnf_price_per_vim'] = price_list
254
255 # pinning to dc
256 if self._pinning is not None:
257 for pinned_vnf in self._pinning:
258 if vnf_profile['id'] == pinned_vnf['member-vnf-index']:
259 vnf_info['vim_account'] = 'vim' + pinned_vnf['vimAccountId'].replace('-', '_')
260
261 ns_desc.append(vnf_info)
262 return ns_desc
263
264 def create_ns_placement_data(self):
265 """populate NsPlacmentData object
266 """
267 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 return ns_placement_data