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