Coverage for manila/scheduler/filters/json.py: 98%

72 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright (c) 2011 OpenStack Foundation. 

2# All Rights Reserved. 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); you may 

5# not use this file except in compliance with the License. You may obtain 

6# a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

13# License for the specific language governing permissions and limitations 

14# under the License. 

15 

16import operator 

17 

18from oslo_serialization import jsonutils 

19 

20from manila.scheduler.filters import base_host 

21 

22 

23class JsonFilter(base_host.BaseHostFilter): 

24 """Host Filter to allow simple JSON-based grammar for selecting hosts.""" 

25 

26 def _op_compare(self, args, op): 

27 """Check if operator can compare the first arg with the others. 

28 

29 Returns True if the specified operator can successfully 

30 compare the first item in the args with all the rest. Will 

31 return False if only one item is in the list. 

32 """ 

33 if len(args) < 2: 

34 return False 

35 if op is operator.contains: 

36 bad = args[0] not in args[1:] 

37 else: 

38 bad = [arg for arg in args[1:] 

39 if not op(args[0], arg)] 

40 return not bool(bad) 

41 

42 def _equals(self, args): 

43 """First term is == all the other terms.""" 

44 return self._op_compare(args, operator.eq) 

45 

46 def _less_than(self, args): 

47 """First term is < all the other terms.""" 

48 return self._op_compare(args, operator.lt) 

49 

50 def _greater_than(self, args): 

51 """First term is > all the other terms.""" 

52 return self._op_compare(args, operator.gt) 

53 

54 def _in(self, args): 

55 """First term is in set of remaining terms.""" 

56 return self._op_compare(args, operator.contains) 

57 

58 def _less_than_equal(self, args): 

59 """First term is <= all the other terms.""" 

60 return self._op_compare(args, operator.le) 

61 

62 def _greater_than_equal(self, args): 

63 """First term is >= all the other terms.""" 

64 return self._op_compare(args, operator.ge) 

65 

66 def _not(self, args): 

67 """Flip each of the arguments.""" 

68 return [not arg for arg in args] 

69 

70 def _or(self, args): 

71 """True if any arg is True.""" 

72 return any(args) 

73 

74 def _and(self, args): 

75 """True if all args are True.""" 

76 return all(args) 

77 

78 commands = { 

79 '=': _equals, 

80 '<': _less_than, 

81 '>': _greater_than, 

82 'in': _in, 

83 '<=': _less_than_equal, 

84 '>=': _greater_than_equal, 

85 'not': _not, 

86 'or': _or, 

87 'and': _and, 

88 } 

89 

90 def _parse_string(self, string, host_state): 

91 """Parse string. 

92 

93 Strings prefixed with $ are capability lookups in the 

94 form '$variable' where 'variable' is an attribute in the 

95 HostState class. If $variable is a dictionary, you may 

96 use: $variable.dictkey 

97 """ 

98 if not string: 98 ↛ 99line 98 didn't jump to line 99 because the condition on line 98 was never true

99 return None 

100 if not string.startswith("$"): 

101 return string 

102 

103 path = string[1:].split(".") 

104 obj = getattr(host_state, path[0], None) 

105 if obj is None: 

106 return None 

107 for item in path[1:]: 

108 obj = obj.get(item) 

109 if obj is None: 

110 return None 

111 return obj 

112 

113 def _process_filter(self, query, host_state): 

114 """Recursively parse the query structure.""" 

115 if not query: 

116 return True 

117 cmd = query[0] 

118 method = self.commands[cmd] 

119 cooked_args = [] 

120 for arg in query[1:]: 

121 if isinstance(arg, list): 

122 arg = self._process_filter(arg, host_state) 

123 elif isinstance(arg, str): 

124 arg = self._parse_string(arg, host_state) 

125 if arg is not None: 

126 cooked_args.append(arg) 

127 result = method(self, cooked_args) 

128 return result 

129 

130 def host_passes(self, host_state, filter_properties): 

131 """Filters hosts. 

132 

133 Return a list of hosts that can fulfill the requirements 

134 specified in the query. 

135 """ 

136 # TODO(zhiteng) Add description for filter_properties structure 

137 # and scheduler_hints. 

138 try: 

139 query = filter_properties['scheduler_hints']['query'] 

140 # If filter_properties['scheduler_hints'] is None, and TypeError 

141 # will occur, add TypeError exception here. 

142 except (KeyError, TypeError): 

143 query = None 

144 if not query: 

145 return True 

146 

147 # NOTE(comstud): Not checking capabilities or service for 

148 # enabled/disabled so that a provided json filter can decide 

149 

150 result = self._process_filter(jsonutils.loads(query), host_state) 

151 if isinstance(result, list): 

152 # If any succeeded, include the host 

153 result = any(result) 

154 if result: 

155 # Filter it out. 

156 return True 

157 return False