Return flavor-id in OOF response
[optf/has.git] / conductor / conductor / data / plugins / inventory_provider / hpa_utils.py
1 #!/usr/bin/env python
2 #
3 # -------------------------------------------------------------------------
4 #   Copyright (c) 2018 Intel Corporation Intellectual Property
5 #
6 #   Licensed under the Apache License, Version 2.0 (the "License");
7 #   you may not use this file except in compliance with the License.
8 #   You may obtain a copy of the License at
9 #
10 #       http://www.apache.org/licenses/LICENSE-2.0
11 #
12 #   Unless required by applicable law or agreed to in writing, software
13 #   distributed under the License is distributed on an "AS IS" BASIS,
14 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 #   See the License for the specific language governing permissions and
16 #   limitations under the License.
17 #
18 # -------------------------------------------------------------------------
19 #
20
21 '''Utility functions for
22    Hardware Platform Awareness (HPA) constraint plugin'''
23
24 import operator
25
26 import conductor.common.prometheus_metrics as PC
27 # python imports
28 import yaml
29 from conductor.i18n import _LI
30 # Third-party library imports
31 from oslo_log import log
32
33 LOG = log.getLogger(__name__)
34
35
36 def  match_all_operator(big_list, small_list):
37     '''
38     Match ALL operator for HPA
39     Check if smaller list is a subset of bigger list
40     :param big_list: bigger list
41     :param small_list: smaller list
42     :return: True or False
43     '''
44     if not big_list or not small_list:
45         return False
46
47     big_set = set(big_list)
48     small_set = set(small_list)
49
50     return small_set.issubset(big_set)
51
52
53 class HpaMatchProvider(object):
54
55     def __init__(self, candidate, req_cap_list):
56         self.flavors_list = candidate['flavors']['flavor']
57         self.req_cap_list = req_cap_list
58         self.m_vim_id = candidate.get('vim-id')
59
60     # Find the flavor which has all the required capabilities
61     def match_flavor(self):
62         # Keys to find capability match
63         hpa_keys = ['hpa-feature', 'architecture', 'hpa-version']
64         req_filter_list = []
65         for capability in CapabilityDataParser.get_item(self.req_cap_list,
66                                                         None):
67             if capability.item['mandatory'] == 'True':
68                 hpa_list = {k: capability.item[k] \
69                             for k in hpa_keys if k in capability.item}
70                 if hpa_list not in req_filter_list:
71                     req_filter_list.append(hpa_list)
72         max_score = -1
73         directives = None
74         for flavor in self.flavors_list:
75             flavor_filter_list = []
76             try:
77                 flavor_cap_list = flavor['hpa-capabilities']
78             except KeyError:
79                 LOG.info(_LI("hpa-capabilities not found in flavor "))
80                 # Metrics to Prometheus
81                 m_flavor_name = flavor['flavor-name']
82                 PC.HPA_FLAVOR_MATCH_UNSUCCESSFUL.labels('ONAP', 'N/A', 'N/A',
83                                                       'N/A', self.m_vim_id,
84                                                       m_flavor_name).inc()
85                 continue
86             for capability in CapabilityDataParser.get_item(flavor_cap_list,
87                                                             'hpa-capability'):
88                 hpa_list = {k: capability.item[k] \
89                                for k in hpa_keys if k in capability.item}
90                 flavor_filter_list.append(hpa_list)
91             # if flavor has the matching capability compare attributes
92             if self._is_cap_supported(flavor_filter_list, req_filter_list):
93                 match_found, score, req_directives = self._compare_feature_attributes(flavor_cap_list)
94                 if match_found:
95                     LOG.info(_LI("Matching Flavor found '{}' for request - {}").
96                              format(flavor['flavor-name'], self.req_cap_list))
97                     # Metrics to Prometheus
98                     m_flavor_name = flavor['flavor-name']
99                     PC.HPA_FLAVOR_MATCH_SUCCESSFUL.labels('ONAP', 'N/A', 'N/A',
100                                                           'N/A', self.m_vim_id,
101                                                           m_flavor_name).inc()
102                     if score > max_score:
103                         max_score = score
104                         flavor_map = {"flavor-id": flavor['flavor-id'],
105                                       "flavor-name": flavor['flavor-name'],
106                                       "score": max_score}
107                         directives = {"flavor_map": flavor_map,
108                                       "directives": req_directives}
109                 else:
110                     # Metrics to Prometheus
111                     m_flavor_name = flavor['flavor-name']
112                     PC.HPA_FLAVOR_MATCH_UNSUCCESSFUL.labels('ONAP', 'N/A',
113                                                             'N/A', 'N/A',
114                                                             self.m_vim_id,
115                                                             m_flavor_name).inc()
116             else:
117                 # Metrics to Prometheus
118                 m_flavor_name = flavor['flavor-name']
119                 PC.HPA_FLAVOR_MATCH_UNSUCCESSFUL.labels('ONAP', 'N/A',
120                                                         'N/A', 'N/A',
121                                                         self.m_vim_id,
122                                                         m_flavor_name).inc()
123         return directives
124
125
126     def _is_cap_supported(self, flavor, cap):
127         try:
128             for elem in cap:
129                 flavor.remove(elem)
130         except ValueError:
131             return False
132         # Found all capabilities in Flavor
133         return True
134
135     # Convert to bytes value using unit
136     def _get_normalized_value(self, unit, value):
137
138         if not value.isdigit():
139             return value
140         value = int(value)
141         if unit == 'KB':
142             value = value * 1024
143         elif unit == 'MB':
144             value = value * 1024 * 1024
145         elif unit == 'GB':
146             value = value * 1024 * 1024 * 1024
147         return str(value)
148
149     def _get_req_attribute(self, req_attr):
150         try:
151             c_op = req_attr['operator']
152             c_value = req_attr['hpa-attribute-value']
153             c_unit = None
154             if 'unit' in req_attr:
155                 c_unit = req_attr['unit']
156         except KeyError:
157             LOG.info(_LI("invalid JSON "))
158             return None
159
160         if c_unit:
161             c_value = self._get_normalized_value(c_unit, c_value)
162         return c_value, c_op
163
164     def _get_flavor_attribute(self, flavor_attr):
165         try:
166             attrib_value = yaml.load(flavor_attr['hpa-attribute-value'])
167         except:
168             return None
169
170         f_unit = None
171         f_value = None
172         for key, value in attrib_value.iteritems():
173             if key == 'value':
174                 f_value = value
175             elif key == 'unit':
176                 f_unit = value
177         if f_unit:
178             f_value = self._get_normalized_value(f_unit, f_value)
179         return f_value
180
181     def _get_operator(self, req_op):
182
183         operator_list = ['=', '<', '>', '<=', '>=', 'ALL']
184
185         if req_op not in operator_list:
186             return None
187
188         if req_op == ">":
189             op = operator.gt
190         elif req_op == ">=":
191             op = operator.ge
192         elif req_op == "<":
193             op = operator.lt
194         elif req_op == "<=":
195             op = operator.le
196         elif req_op == "=":
197             op = operator.eq
198         elif req_op == 'ALL':
199             op = match_all_operator
200
201         return op
202
203
204     def _compare_attribute(self, flavor_attr, req_attr):
205
206         req_value, req_op = self._get_req_attribute(req_attr)
207         flavor_value = self._get_flavor_attribute(flavor_attr)
208
209         if req_value is None or flavor_value is None:
210             return False
211
212         # Compare operators only valid for Integers
213         if req_op in ['<', '>', '<=', '>=']:
214             if not req_value.isdigit() or not flavor_value.isdigit():
215                 return False
216
217         op = self._get_operator(req_op)
218         if not op:
219             return False
220
221         if req_op == 'ALL':
222             # All is valid only for lists
223             if isinstance(req_value, list) and isinstance(flavor_value, list):
224                 return op(flavor_value, req_value)
225
226         # if values are string compare them as strings
227         if req_op == '=':
228             if not req_value.isdigit() or not flavor_value.isdigit():
229                 return op(req_value, flavor_value)
230
231         # Only integers left to compare
232         if req_op in ['<', '>', '<=', '>=', '=']:
233             return  op(int(flavor_value), int(req_value))
234
235         return False
236
237     # for the feature get the capabilty feature attribute list
238     def _get_flavor_cfa_list(self, feature, flavor_cap_list):
239         feature_attr_list = []
240         for capability in CapabilityDataParser.get_item(flavor_cap_list,
241                                                         'hpa-capability'):
242             flavor_feature, feature_attributes = capability.get_fields()
243             # Multiple features that match this condition will be filtered
244             if feature == flavor_feature:
245                 feature_attr_list.append(feature_attributes)
246         return feature_attr_list
247
248     # flavor has all the required capabilties
249     # For each required capability find capability in flavor
250     # and compare each attribute
251     def _compare_feature_attributes(self, flavor_cap_list):
252         score = 0
253         directives = []
254         for capability in CapabilityDataParser.get_item(self.req_cap_list, None):
255             hpa_feature, req_cfa_list = capability.get_fields()
256             feature_directive = capability.get_directives()
257             if feature_directive:
258                 feature_directive[:] = [d for d in feature_directive
259                                         if d.get("type") != ""]
260                 for item in feature_directive:
261                     directives.append(item)
262             flavor_cfa_list = self._get_flavor_cfa_list(hpa_feature, flavor_cap_list)
263             req_flag = False
264             if flavor_cfa_list is not None:
265                 for flavor_cfa in flavor_cfa_list:
266                     flavor_flag = True
267                     for req_feature_attr in req_cfa_list:
268                         req_attr_key = req_feature_attr['hpa-attribute-key']
269                         # filter to get the attribute being compared
270                         flavor_feature_attr = \
271                             filter(lambda ele: ele['hpa-attribute-key'] ==
272                                                req_attr_key, flavor_cfa)
273                         if not flavor_feature_attr:
274                             flavor_flag = False
275                         elif not self._compare_attribute(flavor_feature_attr[0],
276                                                        req_feature_attr):
277                             flavor_flag = False
278                     if not flavor_flag:
279                         continue
280                     else:
281                         req_flag = True
282                         break
283             if not req_flag and capability.item['mandatory'] == 'True':
284                 return False, 0, None
285             if req_flag and capability.item['mandatory'] == 'False':
286                 score = score + int(capability.item['score'])
287         return True, score, directives
288
289
290 class CapabilityDataParser(object):
291     """Helper class to parse  data"""
292
293     def __init__(self, item):
294         self.item = item
295
296     @classmethod
297     def get_item(cls, payload, key):
298         try:
299             if key is None:
300                 features = payload
301             else:
302                 features = (payload[key])
303
304             for f in features:
305                 yield cls(f)
306         except KeyError:
307             LOG.info(_LI("invalid JSON "))
308
309     def get_fields(self):
310         return (self.get_feature(),
311                 self.get_feature_attributes())
312
313     def get_feature_attributes(self):
314         return self.item.get('hpa-feature-attributes')
315
316     def get_feature(self):
317         return self.item.get('hpa-feature')
318
319     def get_directives(self):
320         return self.item.get('directives')