Include j2 files in MANIFEST.in.
[osm/PLA.git] / osm_pla / placement / mznplacement.py
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, PackageLoader, ChoiceLoader
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 './', '/usr/lib/python3/dist-packages/osm_pla/placement']
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"""
115 loader1 = FileSystemLoader(MznModelGenerator.template_search_path)
116 loader2 = PackageLoader('osm_pla', '.')
117 env = Environment(loader=ChoiceLoader([loader1, loader2]))
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():
157 ep1 = pil['pil_endpoints'][0]
158 ep2 = pil['pil_endpoints'][1]
159 # only consider links between applicable vims
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']
163 trp_link_characteristics[idx1][idx2] = pil[characteristics]
164 trp_link_characteristics[idx2][idx1] = pil[characteristics]
165
166 return trp_link_characteristics
167
168 def _produce_vld_desc(self):
169 """
170 Creates the expected vlds from the nsd. Includes constraints if part of nsd.
171 Overrides constraints with any syntactically correct instantiation parameters
172 :return:
173 """
174 vld_desc = []
175 for vld in self._nsd['vld']:
176 if vld.get('mgmt-network', False) is False:
177 vld_desc_entry = {}
178 cp_refs = [ep_ref['member-vnf-index-ref'] for ep_ref in vld['vnfd-connection-point-ref']]
179 if len(cp_refs) == 2:
180 vld_desc_entry['cp_refs'] = cp_refs
181 if 'link-constraint' in vld.keys():
182 for constraint in vld['link-constraint']:
183 if constraint['constraint-type'] == 'LATENCY':
184 vld_desc_entry['latency'] = constraint['value']
185 elif constraint['constraint-type'] == 'JITTER':
186 vld_desc_entry['jitter'] = constraint['value']
187 vld_desc.append(vld_desc_entry)
188
189 # create candidates from instantiate params
190 if self._order_constraints is not None:
191 candidate_vld_desc = []
192 # use id to find the endpoints in the nsd
193 for entry in self._order_constraints.get('vld-constraints'):
194 for vld in self._nsd['vld']:
195 if entry['id'] == vld['id']:
196 vld_desc_instantiate_entry = {}
197 cp_refs = [ep_ref['member-vnf-index-ref'] for ep_ref in vld['vnfd-connection-point-ref']]
198 vld_desc_instantiate_entry['cp_refs'] = cp_refs
199 # add whatever constraints that are provided to the vld_desc_entry
200 # misspelled 'link-constraints' => empty dict
201 # lack (or misspelling) of one or both supported constraints => entry not appended
202 for constraint, value in entry.get('link-constraints', {}).items():
203 if constraint == 'latency':
204 vld_desc_instantiate_entry['latency'] = value
205 elif constraint == 'jitter':
206 vld_desc_instantiate_entry['jitter'] = value
207 if set(['latency', 'jitter']).intersection(vld_desc_instantiate_entry.keys()):
208 candidate_vld_desc.append(vld_desc_instantiate_entry)
209 # merge with nsd originated, FIXME log any deviations?
210 for vld_d in vld_desc:
211 for vld_d_i in candidate_vld_desc:
212 if set(vld_d['cp_refs']) == set(vld_d_i['cp_refs']):
213 if vld_d_i.get('jitter'):
214 vld_d['jitter'] = vld_d_i['jitter']
215 if vld_d_i.get('latency'):
216 vld_d['latency'] = vld_d_i['latency']
217
218 return vld_desc
219
220 def _produce_ns_desc(self):
221 """
222 collect information for the ns_desc part of the placement data
223 for the vim_accounts that are applicable, collect the vnf_price
224 """
225 ns_desc = []
226 for vnfd in self._nsd['constituent-vnfd']:
227 vnf_info = {'vnf_id': vnfd['member-vnf-index']}
228 # prices
229 prices_for_vnfd = self._vnf_prices[vnfd['vnfd-id-ref']]
230 # the list of prices must be ordered according to the indexing of the vim_accounts
231 price_list = [_ for _ in range(len(self._vim_accounts_info))]
232 for k in prices_for_vnfd.keys():
233 if k in self._vim_accounts_info.keys():
234 price_list[self._vim_accounts_info[k]['idx']] = prices_for_vnfd[k]
235 vnf_info['vnf_price_per_vim'] = price_list
236
237 # pinning to dc
238 if self._pinning is not None:
239 for pinned_vnf in self._pinning:
240 if vnfd['member-vnf-index'] == pinned_vnf['member-vnf-index']:
241 vnf_info['vim_account'] = 'vim' + pinned_vnf['vimAccountId'].replace('-', '_')
242
243 ns_desc.append(vnf_info)
244 return ns_desc
245
246 def create_ns_placement_data(self):
247 """populate NsPlacmentData object
248 """
249 ns_placement_data = {'vim_accounts': [vim_data['id'] for _, vim_data in sorted(self._vim_accounts_info.items(),
250 key=lambda item: item[1][
251 'idx'])],
252 'trp_link_latency': self._produce_trp_link_characteristics_data('pil_latency'),
253 'trp_link_jitter': self._produce_trp_link_characteristics_data('pil_jitter'),
254 'trp_link_price_list': self._produce_trp_link_characteristics_data('pil_price'),
255 'ns_desc': self._produce_ns_desc(),
256 'vld_desc': self._produce_vld_desc(),
257 'generator_data': {'file': __file__, 'time': datetime.datetime.now()}}
258
259 return ns_placement_data