# 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, PackageLoader, ChoiceLoader 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': '', '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", "./", "/usr/lib/python3/dist-packages/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""" loader1 = FileSystemLoader(MznModelGenerator.template_search_path) loader2 = PackageLoader("osm_pla", ".") env = Environment(loader=ChoiceLoader([loader1, loader2]), autoescape=True) 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': '', '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(): ep1 = pil["pil_endpoints"][0] ep2 = pil["pil_endpoints"][1] # only consider links between applicable vims if ep1 in self._vim_accounts_info and ep2 in self._vim_accounts_info: idx1 = self._vim_accounts_info[ep1]["idx"] idx2 = self._vim_accounts_info[ep2]["idx"] trp_link_characteristics[idx1][idx2] = pil[characteristics] trp_link_characteristics[idx2][idx1] = pil[characteristics] return trp_link_characteristics # TODO: Review if we should adapt this method with SOL006 /nsd/vnfd/int-virtual-link-desc/df/qos fields. 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: """ all_vld_member_vnf_index_refs = {} # TODO: Change for multiple DF support ns_df = self._nsd.get("df", [{}])[0] for vnf_profile in ns_df.get("vnf-profile", []): for vlc in vnf_profile.get("virtual-link-connectivity", []): vld_id = vlc.get("virtual-link-profile-id") vld_member_vnf_index_ref = vnf_profile.get("id") if vld_id in all_vld_member_vnf_index_refs: all_vld_member_vnf_index_refs[vld_id].append( vld_member_vnf_index_ref ) else: all_vld_member_vnf_index_refs[vld_id] = [vld_member_vnf_index_ref] vld_desc = [] for vld in self._nsd.get("virtual-link-desc", ()): if vld.get("mgmt-network", False) is True: continue vld_desc_entry = {} cp_refs = all_vld_member_vnf_index_refs[vld.get("id")] if len(cp_refs) == 2: vld_desc_entry["cp_refs"] = cp_refs # TODO: Change for multiple DF support vld_df = vld.get("df", [{}])[0] for constraint in vld_df.get("qos", {}): if constraint == "latency": vld_desc_entry["latency"] = vld_df["qos"][constraint] elif constraint == "packet-delay-variation": vld_desc_entry["jitter"] = vld_df["qos"][constraint] 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.get("virtual-link-desc", ()): if entry["id"] == vld.get("id"): vld_desc_instantiate_entry = {} cp_refs = all_vld_member_vnf_index_refs[vld.get("id")] 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 {"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 = [] # TODO: Change for multiple DF support ns_df = self._nsd.get("df", [{}])[0] for vnf_profile in ns_df.get("vnf-profile", []): vnf_info = {"vnf_id": vnf_profile["id"]} # prices prices_for_vnfd = self._vnf_prices[vnf_profile["vnfd-id"]] # 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 vnf_profile["id"] == 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 sorted( self._vim_accounts_info.items(), key=lambda item: item[1]["idx"] ) ], "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