1 # Copyright 2020 ArctosLabs Scandinavia AB
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
20 from jinja2
import Environment
21 from jinja2
.loaders
import FileSystemLoader
24 class MznPlacementConductor(object):
26 Knows how to process placement req using minizinc
28 if platform
.system() == 'Windows':
29 default_mzn_path
= 'C:\\Program Files\\MiniZinc IDE (bundled)\\minizinc.exe'
31 default_mzn_path
= '/minizinc/bin/minizinc'
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?
37 def _run_placement_model(self
, mzn_model
, ns_desc
, mzn_model_data
={}):
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
44 Note: minizinc does not support identifiers starting with numbers and therefore we skip the leading 'vim_'
45 when we process the result
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
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
54 solns
= pymzn
.minizinc(mzn_model
, data
=mzn_model_data
, output_mode
='item')
56 if 'UNSATISFIABLE' in str(solns
):
59 solns_as_str
= str(solns
[0])
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(';')
64 vnf_vim_mapping
= (e
.split('=') for e
in solns_as_str
.split(';'))
66 res
= [{'vimAccountId': e
[1][3:].replace('_', '-'), 'member-vnf-index': e
[0][3:]} for e
in
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()]
74 def do_placement_computation(self
, nspd
):
76 Orchestrates the placement computation
78 :param nspd: placement data
79 :return: see _run_placement_model
81 mzn_model
= MznModelGenerator(self
.log
).create_model(nspd
)
82 return self
._run
_placement
_model
(mzn_model
, nspd
['ns_desc'])
85 class MznModelGenerator(object):
87 Has the capability to generate minizinc models from information contained in
88 NsPlacementData objects. Uses jinja2 as templating language for the model
90 default_j2_template
= "osm_pla_dynamic_template.j2"
91 template_search_path
= ['osm_pla/placement', '../placement', '/pla/osm_pla/placement']
93 def __init__(self
, log
):
97 self
.log
= log
# FIXME we do not log anything so far
99 def create_model(self
, ns_placement_data
):
101 Creates a minizinc model according to the content of nspd
102 nspd - NSPlacementData
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
))
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
)
118 class NsPlacementDataFactory(object):
120 process information an network service and applicable network infrastructure resources in order to produce
121 information tailored for the minizinc model code generator
124 def __init__(self
, vim_accounts_info
, vnf_prices
, nsd
, pil_info
, pinning
=None, order_constraints
=None):
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
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
139 self
._pil
_info
= pil_info
140 self
._pinning
= pinning
141 self
._order
_constraints
= order_constraints
143 def _produce_trp_link_characteristics_data(self
, characteristics
):
145 :param characteristics: one of {pil_latency, pil_price, pil_jitter}
146 :return: 2d array of requested trp_link characteristics data
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
]
163 return trp_link_characteristics
165 def _produce_vld_desc(self
):
167 Creates the expected vlds from the nsd. Includes constraints if part of nsd.
168 Overrides constraints with any syntactically correct instantiation parameters
172 for vld
in self
._nsd
['vld']:
173 if vld
.get('mgmt-network', False) is False:
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
)
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']
217 def _produce_ns_desc(self
):
219 collect information for the ns_desc part of the placement data
220 for the vim_accounts that are applicable, collect the vnf_price
223 for vnfd
in self
._nsd
['constituent-vnfd']:
224 vnf_info
= {'vnf_id': vnfd
['member-vnf-index']}
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
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('-', '_')
240 ns_desc
.append(vnf_info
)
243 def create_ns_placement_data(self
):
244 """populate NsPlacmentData object
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][
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()}}
256 return ns_placement_data