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