| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 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 |
| magnussonl | de4f782 | 2020-07-09 16:30:41 +0200 | [diff] [blame] | 21 | from jinja2.loaders import FileSystemLoader, PackageLoader, ChoiceLoader |
| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 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" |
| magnussonl | de4f782 | 2020-07-09 16:30:41 +0200 | [diff] [blame] | 91 | template_search_path = ['osm_pla/placement', '../placement', '/pla/osm_pla/placement', |
| 92 | './', '/usr/lib/python3/dist-packages/osm_pla/placement'] |
| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 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""" |
| magnussonl | de4f782 | 2020-07-09 16:30:41 +0200 | [diff] [blame] | 115 | loader1 = FileSystemLoader(MznModelGenerator.template_search_path) |
| 116 | loader2 = PackageLoader('osm_pla', '.') |
| 117 | env = Environment(loader=ChoiceLoader([loader1, loader2])) |
| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 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(): |
| magnussonl | d8c1b39 | 2020-06-30 16:48:08 +0200 | [diff] [blame] | 157 | ep1 = pil['pil_endpoints'][0] |
| 158 | ep2 = pil['pil_endpoints'][1] |
| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 159 | # only consider links between applicable vims |
| magnussonl | d8c1b39 | 2020-06-30 16:48:08 +0200 | [diff] [blame] | 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'] |
| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 163 | trp_link_characteristics[idx1][idx2] = pil[characteristics] |
| 164 | trp_link_characteristics[idx2][idx1] = pil[characteristics] |
| 165 | |
| 166 | return trp_link_characteristics |
| 167 | |
| garciaale | b2b0a44 | 2021-01-08 14:59:23 -0300 | [diff] [blame] | 168 | # TODO: Review if we should adapt this method with SOL006 /nsd/vnfd/int-virtual-link-desc/df/qos fields. |
| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 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 | """ |
| garciaale | b2b0a44 | 2021-01-08 14:59:23 -0300 | [diff] [blame] | 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 | |
| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 188 | vld_desc = [] |
| garciaale | b2b0a44 | 2021-01-08 14:59:23 -0300 | [diff] [blame] | 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) |
| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 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'): |
| garciaale | b2b0a44 | 2021-01-08 14:59:23 -0300 | [diff] [blame] | 210 | for vld in self._nsd.get('virtual-link-desc', ()): |
| 211 | if entry['id'] == vld.get('id'): |
| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 212 | vld_desc_instantiate_entry = {} |
| garciaale | b2b0a44 | 2021-01-08 14:59:23 -0300 | [diff] [blame] | 213 | cp_refs = all_vld_member_vnf_index_refs[vld.get('id')] |
| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 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 |
| garciaale | b2b0a44 | 2021-01-08 14:59:23 -0300 | [diff] [blame] | 223 | if {'latency', 'jitter'}.intersection(vld_desc_instantiate_entry.keys()): |
| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 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 = [] |
| garciaale | b2b0a44 | 2021-01-08 14:59:23 -0300 | [diff] [blame] | 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']} |
| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 246 | # prices |
| garciaale | b2b0a44 | 2021-01-08 14:59:23 -0300 | [diff] [blame] | 247 | prices_for_vnfd = self._vnf_prices[vnf_profile['vnfd-id']] |
| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 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: |
| garciaale | b2b0a44 | 2021-01-08 14:59:23 -0300 | [diff] [blame] | 258 | if vnf_profile['id'] == pinned_vnf['member-vnf-index']: |
| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 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 | """ |
| magnussonl | d8c1b39 | 2020-06-30 16:48:08 +0200 | [diff] [blame] | 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'])], |
| magnussonl | 2b0e2d7 | 2020-02-04 10:52:46 +0100 | [diff] [blame] | 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 |