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