Ignore flavors that don't have hpa-capabilities
[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 # python imports
25 import yaml
26 import operator
27
28 from conductor.i18n import _LE, _LI
29
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
59     # Find the flavor which has all the required capabilities
60     def match_flavor(self):
61         # Keys to find capability match
62         hpa_keys = ['hpa-feature', 'architecture', 'hpa-version']
63         req_filter_list = []
64         for capability in CapabilityDataParser.get_item(self.req_cap_list,
65                                                         None):
66             if capability.item['mandatory'] == 'True':
67                 hpa_list = {k: capability.item[k] \
68                             for k in hpa_keys if k in capability.item}
69                 req_filter_list.append(hpa_list)
70         max_score = -1
71         flavor_map = None
72         for flavor in self.flavors_list:
73             flavor_filter_list = []
74             try:
75                 flavor_cap_list = flavor['hpa-capabilities']
76             except KeyError:
77                 LOG.info(_LI("hpa-capabilities not found in flavor "))
78                 continue
79             for capability in CapabilityDataParser.get_item(flavor_cap_list,
80                                                             'hpa-capability'):
81                 hpa_list = {k: capability.item[k] \
82                                for k in hpa_keys if k in capability.item}
83                 flavor_filter_list.append(hpa_list)
84             # if flavor has the matching capability compare attributes
85             if self._is_cap_supported(flavor_filter_list, req_filter_list):
86                 match_found, score = self._compare_feature_attributes(flavor_cap_list)
87                 if match_found:
88                     LOG.info(_LI("Matching Flavor found '{}' for request - {}").
89                              format(flavor['flavor-name'], self.req_cap_list))
90                     if score > max_score:
91                         max_score = score
92                         flavor_map = {"flavor-id": flavor['flavor-id'],
93                                   "flavor-name": flavor['flavor-name']}
94         return flavor_map
95
96
97     def _is_cap_supported(self, flavor, cap):
98         try:
99             for elem in cap:
100                 flavor.remove(elem)
101         except ValueError:
102             return False
103         # Found all capabilities in Flavor
104         return True
105
106     # Convert to bytes value using unit
107     def _get_normalized_value(self, unit, value):
108
109         if not value.isdigit():
110             return value
111         value = int(value)
112         if unit == 'KB':
113             value = value * 1024
114         elif unit == 'MB':
115             value = value * 1024 * 1024
116         elif unit == 'GB':
117             value = value * 1024 * 1024 * 1024
118         return str(value)
119
120     def _get_req_attribute(self, req_attr):
121         try:
122             c_op = req_attr['operator']
123             c_value = req_attr['hpa-attribute-value']
124             c_unit = None
125             if 'unit' in req_attr:
126                 c_unit = req_attr['unit']
127         except KeyError:
128             LOG.info(_LI("invalid JSON "))
129             return None
130
131         if c_unit:
132             c_value = self._get_normalized_value(c_unit, c_value)
133         return c_value, c_op
134
135     def _get_flavor_attribute(self, flavor_attr):
136         try:
137             attrib_value = yaml.load(flavor_attr['hpa-attribute-value'])
138         except:
139             return None
140
141         f_unit = None
142         f_value = None
143         for key, value in attrib_value.iteritems():
144             if key == 'value':
145                 f_value = value
146             elif key == 'unit':
147                 f_unit = value
148         if f_unit:
149             f_value = self._get_normalized_value(f_unit, f_value)
150         return f_value
151
152     def _get_operator(self, req_op):
153
154         operator_list = ['=', '<', '>', '<=', '>=', 'ALL']
155
156         if req_op not in operator_list:
157             return None
158
159         if req_op == ">":
160             op = operator.gt
161         elif req_op == ">=":
162             op = operator.ge
163         elif req_op == "<":
164             op = operator.lt
165         elif req_op == "<=":
166             op = operator.le
167         elif req_op == "=":
168             op = operator.eq
169         elif req_op == 'ALL':
170             op = match_all_operator
171
172         return op
173
174
175     def _compare_attribute(self, flavor_attr, req_attr):
176
177         req_value, req_op = self._get_req_attribute(req_attr)
178         flavor_value = self._get_flavor_attribute(flavor_attr)
179
180         if req_value is None or flavor_value is None:
181             return False
182
183         # Compare operators only valid for Integers
184         if req_op in ['<', '>', '<=', '>=']:
185             if not req_value.isdigit() or not flavor_value.isdigit():
186                 return False
187
188         op = self._get_operator(req_op)
189         if not op:
190             return False
191
192         if req_op == 'ALL':
193             # All is valid only for lists
194             if isinstance(req_value, list) and isinstance(flavor_value, list):
195                 return op(flavor_value, req_value)
196
197         # if values are string compare them as strings
198         if req_op == '=':
199             if not req_value.isdigit() or not flavor_value.isdigit():
200                 return op(req_value, flavor_value)
201
202         # Only integers left to compare
203         if req_op in ['<', '>', '<=', '>=', '=']:
204             return  op(int(flavor_value), int(req_value))
205
206         return False
207
208     # for the feature get the capabilty feature attribute list
209     def _get_flavor_cfa_list(self, feature, flavor_cap_list):
210         for capability in CapabilityDataParser.get_item(flavor_cap_list,
211                                                         'hpa-capability'):
212             flavor_feature, feature_attributes = capability.get_fields()
213             # One feature will match this condition as we have pre-filtered
214             if feature == flavor_feature:
215                 return feature_attributes
216
217     # flavor has all the required capabilties
218     # For each required capability find capability in flavor
219     # and compare each attribute
220     def _compare_feature_attributes(self, flavor_cap_list):
221         score = 0
222         for capability in CapabilityDataParser.get_item(self.req_cap_list, None):
223             hpa_feature, req_cfa_list = capability.get_fields()
224             flavor_cfa_list = self._get_flavor_cfa_list(hpa_feature, flavor_cap_list)
225             if flavor_cfa_list is not None:
226                 for req_feature_attr in req_cfa_list:
227                     req_attr_key = req_feature_attr['hpa-attribute-key']
228                      # filter to get the attribute being compared
229                     flavor_feature_attr = \
230                         filter(lambda ele: ele['hpa-attribute-key'] == \
231                                req_attr_key, flavor_cfa_list)
232                     if not flavor_feature_attr:
233                         return False, 0
234                     if not self._compare_attribute(flavor_feature_attr[0],
235                                                    req_feature_attr):
236                         return False, 0
237             if flavor_cfa_list is not None and capability.item['mandatory'] == 'False':
238                 score = score + int(capability.item['score'])
239         return True, score
240
241
242 class CapabilityDataParser(object):
243     """Helper class to parse  data"""
244
245     def __init__(self, item):
246         self.item = item
247
248     @classmethod
249     def get_item(cls, payload, key):
250         try:
251             if key is None:
252                 features = payload
253             else:
254                 features = (payload[key])
255
256             for f in features:
257                 yield cls(f)
258         except KeyError:
259             LOG.info(_LI("invalid JSON "))
260
261     def get_fields(self):
262         return (self.get_feature(),
263                 self.get_feature_attributes())
264
265     def get_feature_attributes(self):
266         return self.item.get('hpa-feature-attributes')
267
268     def get_feature(self):
269         return self.item.get('hpa-feature')