Initial commit to gerrit repo
Made non-functional change to trigger Jenkins
Add/update license headers
Dockerfile modification
Corrected Dockerfile faults
Dockerfile update
Yet another Dockerfile update
Support for placement without vld:s
Change-Id: I63c1733656f682233c96f6bcadeac1a2765ed085
Signed-off-by: magnussonl <lars-goran.magnusson@arctoslabs.com>
diff --git a/osm_pla/placement/mznplacement.py b/osm_pla/placement/mznplacement.py
new file mode 100755
index 0000000..cf6236a
--- /dev/null
+++ b/osm_pla/placement/mznplacement.py
@@ -0,0 +1,254 @@
+# 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