| 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 |
| 21 | from jinja2.loaders import FileSystemLoader |
| 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 | |
| 93 | def __init__(self, log): |
| 94 | ''' |
| 95 | Constructor |
| 96 | ''' |
| 97 | self.log = log # FIXME we do not log anything so far |
| 98 | |
| 99 | def create_model(self, ns_placement_data): |
| 100 | ''' |
| 101 | Creates a minizinc model according to the content of nspd |
| 102 | nspd - NSPlacementData |
| 103 | return MZNModel |
| 104 | ''' |
| 105 | self.log.info('ns_desc: {}'.format(ns_placement_data['ns_desc'])) |
| 106 | self.log.info('vld_desc: {}'.format(ns_placement_data['vld_desc'])) |
| 107 | mzn_model_template = self._load_jinja_template() |
| 108 | mzn_model = mzn_model_template.render(ns_placement_data) |
| 109 | self.log.info('Minizinc model: {}'.format(mzn_model)) |
| 110 | return mzn_model |
| 111 | |
| 112 | def _load_jinja_template(self, template_name=default_j2_template): |
| 113 | """loads the jinja template used for model generation""" |
| 114 | env = Environment(loader=FileSystemLoader(MznModelGenerator.template_search_path)) |
| 115 | return env.get_template(template_name) |
| 116 | |
| 117 | |
| 118 | class NsPlacementDataFactory(object): |
| 119 | """ |
| 120 | process information an network service and applicable network infrastructure resources in order to produce |
| 121 | information tailored for the minizinc model code generator |
| 122 | """ |
| 123 | |
| 124 | def __init__(self, vim_accounts_info, vnf_prices, nsd, pil_info, pinning=None, order_constraints=None): |
| 125 | """ |
| 126 | :param vim_accounts_info: a dictionary with vim url as key and id as value, we add a unique index to it for use |
| 127 | in the mzn array constructs and adjust the value of the id to minizinc acceptable identifier syntax |
| 128 | :param vnf_prices: a dictionary with 'vnfd-id-ref' as key and a dictionary with vim_urls: cost as value |
| 129 | :param nsd: the network service descriptor |
| 130 | :param pil_info: price list and metrics for PoP interconnection links |
| 131 | :param pinning: list of {'member-vnf-index': '<idx>', 'vim_account': '<vim-account>'} |
| 132 | :param order_constraints: any constraints provided at instantiation time |
| 133 | """ |
| 134 | next_idx = itertools.count() |
| 135 | self._vim_accounts_info = {k: {'id': 'vim' + v.replace('-', '_'), 'idx': next(next_idx)} for k, v in |
| 136 | vim_accounts_info.items()} |
| 137 | self._vnf_prices = vnf_prices |
| 138 | self._nsd = nsd |
| 139 | self._pil_info = pil_info |
| 140 | self._pinning = pinning |
| 141 | self._order_constraints = order_constraints |
| 142 | |
| 143 | def _produce_trp_link_characteristics_data(self, characteristics): |
| 144 | """ |
| 145 | :param characteristics: one of {pil_latency, pil_price, pil_jitter} |
| 146 | :return: 2d array of requested trp_link characteristics data |
| 147 | """ |
| 148 | if characteristics not in {'pil_latency', 'pil_price', 'pil_jitter'}: |
| 149 | raise Exception('characteristic \'{}\' not supported'.format(characteristics)) |
| 150 | num_vims = len(self._vim_accounts_info) |
| 151 | trp_link_characteristics = [[0 if col == row else 0x7fff for col in range(num_vims)] for row in range(num_vims)] |
| 152 | for pil in self._pil_info['pil']: |
| 153 | if characteristics in pil.keys(): |
| 154 | url1 = pil['pil_endpoints'][0] |
| 155 | url2 = pil['pil_endpoints'][1] |
| 156 | # only consider links between applicable vims |
| 157 | if url1 in self._vim_accounts_info and url2 in self._vim_accounts_info: |
| 158 | idx1 = self._vim_accounts_info[url1]['idx'] |
| 159 | idx2 = self._vim_accounts_info[url2]['idx'] |
| 160 | trp_link_characteristics[idx1][idx2] = pil[characteristics] |
| 161 | trp_link_characteristics[idx2][idx1] = pil[characteristics] |
| 162 | |
| 163 | return trp_link_characteristics |
| 164 | |
| 165 | def _produce_vld_desc(self): |
| 166 | """ |
| 167 | Creates the expected vlds from the nsd. Includes constraints if part of nsd. |
| 168 | Overrides constraints with any syntactically correct instantiation parameters |
| 169 | :return: |
| 170 | """ |
| 171 | vld_desc = [] |
| 172 | for vld in self._nsd['vld']: |
| 173 | if vld['mgmt-network'] is False: |
| 174 | vld_desc_entry = {} |
| 175 | cp_refs = [ep_ref['member-vnf-index-ref'] for ep_ref in vld['vnfd-connection-point-ref']] |
| 176 | vld_desc_entry['cp_refs'] = cp_refs |
| 177 | if 'link-constraint' in vld.keys(): |
| 178 | for constraint in vld['link-constraint']: |
| 179 | if constraint['constraint-type'] == 'LATENCY': |
| 180 | vld_desc_entry['latency'] = constraint['value'] |
| 181 | elif constraint['constraint-type'] == 'JITTER': |
| 182 | vld_desc_entry['jitter'] = constraint['value'] |
| 183 | vld_desc.append(vld_desc_entry) |
| 184 | |
| 185 | # create candidates from instantiate params |
| 186 | if self._order_constraints is not None: |
| 187 | candidate_vld_desc = [] |
| 188 | # use id to find the endpoints in the nsd |
| 189 | for entry in self._order_constraints.get('vld-constraints'): |
| 190 | for vld in self._nsd['vld']: |
| 191 | if entry['id'] == vld['id']: |
| 192 | vld_desc_instantiate_entry = {} |
| 193 | cp_refs = [ep_ref['member-vnf-index-ref'] for ep_ref in vld['vnfd-connection-point-ref']] |
| 194 | vld_desc_instantiate_entry['cp_refs'] = cp_refs |
| 195 | # add whatever constraints that are provided to the vld_desc_entry |
| 196 | # misspelled 'link-constraints' => empty dict |
| 197 | # lack (or misspelling) of one or both supported constraints => entry not appended |
| 198 | for constraint, value in entry.get('link-constraints', {}).items(): |
| 199 | if constraint == 'latency': |
| 200 | vld_desc_instantiate_entry['latency'] = value |
| 201 | elif constraint == 'jitter': |
| 202 | vld_desc_instantiate_entry['jitter'] = value |
| 203 | if set(['latency', 'jitter']).intersection(vld_desc_instantiate_entry.keys()): |
| 204 | candidate_vld_desc.append(vld_desc_instantiate_entry) |
| 205 | # merge with nsd originated, FIXME log any deviations? |
| 206 | for vld_d in vld_desc: |
| 207 | for vld_d_i in candidate_vld_desc: |
| 208 | if set(vld_d['cp_refs']) == set(vld_d_i['cp_refs']): |
| 209 | if vld_d_i.get('jitter'): |
| 210 | vld_d['jitter'] = vld_d_i['jitter'] |
| 211 | if vld_d_i.get('latency'): |
| 212 | vld_d['latency'] = vld_d_i['latency'] |
| 213 | |
| 214 | return vld_desc |
| 215 | |
| 216 | def _produce_ns_desc(self): |
| 217 | """ |
| 218 | collect information for the ns_desc part of the placement data |
| 219 | for the vim_accounts that are applicable, collect the vnf_price |
| 220 | """ |
| 221 | ns_desc = [] |
| 222 | for vnfd in self._nsd['constituent-vnfd']: |
| 223 | vnf_info = {'vnf_id': vnfd['member-vnf-index']} |
| 224 | # prices |
| 225 | prices_for_vnfd = self._vnf_prices[vnfd['vnfd-id-ref']] |
| 226 | # the list of prices must be ordered according to the indexing of the vim_accounts |
| 227 | price_list = [_ for _ in range(len(self._vim_accounts_info))] |
| 228 | for k in prices_for_vnfd.keys(): |
| 229 | if k in self._vim_accounts_info.keys(): |
| 230 | price_list[self._vim_accounts_info[k]['idx']] = prices_for_vnfd[k] |
| 231 | vnf_info['vnf_price_per_vim'] = price_list |
| 232 | |
| 233 | # pinning to dc |
| 234 | if self._pinning is not None: |
| 235 | for pinned_vnf in self._pinning: |
| 236 | if vnfd['member-vnf-index'] == pinned_vnf['member-vnf-index']: |
| 237 | vnf_info['vim_account'] = 'vim' + pinned_vnf['vimAccountId'].replace('-', '_') |
| 238 | |
| 239 | ns_desc.append(vnf_info) |
| 240 | return ns_desc |
| 241 | |
| 242 | def create_ns_placement_data(self): |
| 243 | """populate NsPlacmentData object |
| 244 | """ |
| 245 | ns_placement_data = {'vim_accounts': [vim_data['id'] for |
| 246 | vim_data in self._vim_accounts_info.values()], |
| 247 | 'trp_link_latency': self._produce_trp_link_characteristics_data('pil_latency'), |
| 248 | 'trp_link_jitter': self._produce_trp_link_characteristics_data('pil_jitter'), |
| 249 | 'trp_link_price_list': self._produce_trp_link_characteristics_data('pil_price'), |
| 250 | 'ns_desc': self._produce_ns_desc(), |
| 251 | 'vld_desc': self._produce_vld_desc(), |
| 252 | 'generator_data': {'file': __file__, 'time': datetime.datetime.now()}} |
| 253 | |
| 254 | return ns_placement_data |