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 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