| # Copyright 2020 ArctosLabs Scandinavia AB |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
| # implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| import datetime |
| import platform |
| import itertools |
| |
| import pymzn |
| from jinja2 import Environment |
| from jinja2.loaders import FileSystemLoader |
| |
| |
| class MznPlacementConductor(object): |
| """ |
| Knows how to process placement req using minizinc |
| """ |
| if platform.system() == 'Windows': |
| default_mzn_path = 'C:\\Program Files\\MiniZinc IDE (bundled)\\minizinc.exe' |
| else: |
| default_mzn_path = '/minizinc/bin/minizinc' |
| |
| def __init__(self, log, mzn_path=default_mzn_path): |
| pymzn.config['minizinc'] = mzn_path |
| self.log = log # FIXME what to log (besides forwarding it to MznModelGenerator) here? |
| |
| def _run_placement_model(self, mzn_model, ns_desc, mzn_model_data={}): |
| """ |
| Runs the minizinc placement model and post process the result |
| Note: in this revision we use the 'item' output mode from pymzn.minizinc since it ease |
| post processing of the solutions when we use enumerations in mzn_model |
| Note: minizinc does not support '-' in identifiers and therefore we convert back from use of '_' when we |
| process the result |
| Note: minizinc does not support identifiers starting with numbers and therefore we skip the leading 'vim_' |
| when we process the result |
| |
| :param mzn_model: a minizinc model as str (note: may also be path to .mzn file) |
| :param ns_desc: network service descriptor, carries information about pinned VNFs so those can be included in |
| the result |
| :param mzn_model_data: minizinc model data dictionary (typically not used with our models) |
| :return: list of dicts formatted as {'vimAccountId': '<account id>', 'member-vnf-index': <'index'>} |
| or formatted as [{}] if unsatisfiable model |
| """ |
| solns = pymzn.minizinc(mzn_model, data=mzn_model_data, output_mode='item') |
| |
| if 'UNSATISFIABLE' in str(solns): |
| return [{}] |
| |
| solns_as_str = str(solns[0]) |
| |
| # make it easier to extract the desired information by cleaning from newline, whitespace etc. |
| solns_as_str = solns_as_str.replace('\n', '').replace(' ', '').rstrip(';') |
| |
| vnf_vim_mapping = (e.split('=') for e in solns_as_str.split(';')) |
| |
| res = [{'vimAccountId': e[1][3:].replace('_', '-'), 'member-vnf-index': e[0][3:]} for e in |
| vnf_vim_mapping] |
| # add any pinned VNFs |
| pinned = [{'vimAccountId': e['vim_account'][3:].replace('_', '-'), 'member-vnf-index': e['vnf_id']} for e in |
| ns_desc if 'vim_account' in e.keys()] |
| |
| return res + pinned |
| |
| def do_placement_computation(self, nspd): |
| """ |
| Orchestrates the placement computation |
| |
| :param nspd: placement data |
| :return: see _run_placement_model |
| """ |
| mzn_model = MznModelGenerator(self.log).create_model(nspd) |
| return self._run_placement_model(mzn_model, nspd['ns_desc']) |
| |
| |
| class MznModelGenerator(object): |
| ''' |
| Has the capability to generate minizinc models from information contained in |
| NsPlacementData objects. Uses jinja2 as templating language for the model |
| ''' |
| default_j2_template = "osm_pla_dynamic_template.j2" |
| template_search_path = ['osm_pla/placement', '../placement', '/pla/osm_pla/placement'] |
| |
| def __init__(self, log): |
| ''' |
| Constructor |
| ''' |
| self.log = log # FIXME we do not log anything so far |
| |
| def create_model(self, ns_placement_data): |
| ''' |
| Creates a minizinc model according to the content of nspd |
| nspd - NSPlacementData |
| return MZNModel |
| ''' |
| self.log.info('ns_desc: {}'.format(ns_placement_data['ns_desc'])) |
| self.log.info('vld_desc: {}'.format(ns_placement_data['vld_desc'])) |
| mzn_model_template = self._load_jinja_template() |
| mzn_model = mzn_model_template.render(ns_placement_data) |
| self.log.info('Minizinc model: {}'.format(mzn_model)) |
| return mzn_model |
| |
| def _load_jinja_template(self, template_name=default_j2_template): |
| """loads the jinja template used for model generation""" |
| env = Environment(loader=FileSystemLoader(MznModelGenerator.template_search_path)) |
| return env.get_template(template_name) |
| |
| |
| class NsPlacementDataFactory(object): |
| """ |
| process information an network service and applicable network infrastructure resources in order to produce |
| information tailored for the minizinc model code generator |
| """ |
| |
| def __init__(self, vim_accounts_info, vnf_prices, nsd, pil_info, pinning=None, order_constraints=None): |
| """ |
| :param vim_accounts_info: a dictionary with vim url as key and id as value, we add a unique index to it for use |
| in the mzn array constructs and adjust the value of the id to minizinc acceptable identifier syntax |
| :param vnf_prices: a dictionary with 'vnfd-id-ref' as key and a dictionary with vim_urls: cost as value |
| :param nsd: the network service descriptor |
| :param pil_info: price list and metrics for PoP interconnection links |
| :param pinning: list of {'member-vnf-index': '<idx>', 'vim_account': '<vim-account>'} |
| :param order_constraints: any constraints provided at instantiation time |
| """ |
| next_idx = itertools.count() |
| self._vim_accounts_info = {k: {'id': 'vim' + v.replace('-', '_'), 'idx': next(next_idx)} for k, v in |
| vim_accounts_info.items()} |
| self._vnf_prices = vnf_prices |
| self._nsd = nsd |
| self._pil_info = pil_info |
| self._pinning = pinning |
| self._order_constraints = order_constraints |
| |
| def _produce_trp_link_characteristics_data(self, characteristics): |
| """ |
| :param characteristics: one of {pil_latency, pil_price, pil_jitter} |
| :return: 2d array of requested trp_link characteristics data |
| """ |
| if characteristics not in {'pil_latency', 'pil_price', 'pil_jitter'}: |
| raise Exception('characteristic \'{}\' not supported'.format(characteristics)) |
| num_vims = len(self._vim_accounts_info) |
| trp_link_characteristics = [[0 if col == row else 0x7fff for col in range(num_vims)] for row in range(num_vims)] |
| for pil in self._pil_info['pil']: |
| if characteristics in pil.keys(): |
| url1 = pil['pil_endpoints'][0] |
| url2 = pil['pil_endpoints'][1] |
| # only consider links between applicable vims |
| if url1 in self._vim_accounts_info and url2 in self._vim_accounts_info: |
| idx1 = self._vim_accounts_info[url1]['idx'] |
| idx2 = self._vim_accounts_info[url2]['idx'] |
| trp_link_characteristics[idx1][idx2] = pil[characteristics] |
| trp_link_characteristics[idx2][idx1] = pil[characteristics] |
| |
| return trp_link_characteristics |
| |
| def _produce_vld_desc(self): |
| """ |
| Creates the expected vlds from the nsd. Includes constraints if part of nsd. |
| Overrides constraints with any syntactically correct instantiation parameters |
| :return: |
| """ |
| vld_desc = [] |
| for vld in self._nsd['vld']: |
| if vld['mgmt-network'] is False: |
| vld_desc_entry = {} |
| cp_refs = [ep_ref['member-vnf-index-ref'] for ep_ref in vld['vnfd-connection-point-ref']] |
| vld_desc_entry['cp_refs'] = cp_refs |
| if 'link-constraint' in vld.keys(): |
| for constraint in vld['link-constraint']: |
| if constraint['constraint-type'] == 'LATENCY': |
| vld_desc_entry['latency'] = constraint['value'] |
| elif constraint['constraint-type'] == 'JITTER': |
| vld_desc_entry['jitter'] = constraint['value'] |
| vld_desc.append(vld_desc_entry) |
| |
| # create candidates from instantiate params |
| if self._order_constraints is not None: |
| candidate_vld_desc = [] |
| # use id to find the endpoints in the nsd |
| for entry in self._order_constraints.get('vld-constraints'): |
| for vld in self._nsd['vld']: |
| if entry['id'] == vld['id']: |
| vld_desc_instantiate_entry = {} |
| cp_refs = [ep_ref['member-vnf-index-ref'] for ep_ref in vld['vnfd-connection-point-ref']] |
| vld_desc_instantiate_entry['cp_refs'] = cp_refs |
| # add whatever constraints that are provided to the vld_desc_entry |
| # misspelled 'link-constraints' => empty dict |
| # lack (or misspelling) of one or both supported constraints => entry not appended |
| for constraint, value in entry.get('link-constraints', {}).items(): |
| if constraint == 'latency': |
| vld_desc_instantiate_entry['latency'] = value |
| elif constraint == 'jitter': |
| vld_desc_instantiate_entry['jitter'] = value |
| if set(['latency', 'jitter']).intersection(vld_desc_instantiate_entry.keys()): |
| candidate_vld_desc.append(vld_desc_instantiate_entry) |
| # merge with nsd originated, FIXME log any deviations? |
| for vld_d in vld_desc: |
| for vld_d_i in candidate_vld_desc: |
| if set(vld_d['cp_refs']) == set(vld_d_i['cp_refs']): |
| if vld_d_i.get('jitter'): |
| vld_d['jitter'] = vld_d_i['jitter'] |
| if vld_d_i.get('latency'): |
| vld_d['latency'] = vld_d_i['latency'] |
| |
| return vld_desc |
| |
| def _produce_ns_desc(self): |
| """ |
| collect information for the ns_desc part of the placement data |
| for the vim_accounts that are applicable, collect the vnf_price |
| """ |
| ns_desc = [] |
| for vnfd in self._nsd['constituent-vnfd']: |
| vnf_info = {'vnf_id': vnfd['member-vnf-index']} |
| # prices |
| prices_for_vnfd = self._vnf_prices[vnfd['vnfd-id-ref']] |
| # the list of prices must be ordered according to the indexing of the vim_accounts |
| price_list = [_ for _ in range(len(self._vim_accounts_info))] |
| for k in prices_for_vnfd.keys(): |
| if k in self._vim_accounts_info.keys(): |
| price_list[self._vim_accounts_info[k]['idx']] = prices_for_vnfd[k] |
| vnf_info['vnf_price_per_vim'] = price_list |
| |
| # pinning to dc |
| if self._pinning is not None: |
| for pinned_vnf in self._pinning: |
| if vnfd['member-vnf-index'] == pinned_vnf['member-vnf-index']: |
| vnf_info['vim_account'] = 'vim' + pinned_vnf['vimAccountId'].replace('-', '_') |
| |
| ns_desc.append(vnf_info) |
| return ns_desc |
| |
| def create_ns_placement_data(self): |
| """populate NsPlacmentData object |
| """ |
| ns_placement_data = {'vim_accounts': [vim_data['id'] for |
| vim_data in self._vim_accounts_info.values()], |
| 'trp_link_latency': self._produce_trp_link_characteristics_data('pil_latency'), |
| 'trp_link_jitter': self._produce_trp_link_characteristics_data('pil_jitter'), |
| 'trp_link_price_list': self._produce_trp_link_characteristics_data('pil_price'), |
| 'ns_desc': self._produce_ns_desc(), |
| 'vld_desc': self._produce_vld_desc(), |
| 'generator_data': {'file': __file__, 'time': datetime.datetime.now()}} |
| |
| return ns_placement_data |