Coverage for /Users/buh/.pyenv/versions/3.12.9/envs/es-testbed/lib/python3.12/site-packages/es_testbed/_plan.py: 100%

82 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-17 22:23 -0600

1"""TestPlan Class Definition""" 

2 

3import typing as t 

4import logging 

5from dotmap import DotMap 

6from .debug import debug, begin_end 

7from .defaults import TESTPLAN 

8from .utils import build_ilm_policy, prettystr, randomstr 

9 

10logger = logging.getLogger(__name__) 

11 

12 

13class PlanBuilder: 

14 """ 

15 Plan builder class 

16 

17 """ 

18 

19 def __init__( 

20 self, 

21 settings: t.Dict, 

22 autobuild: t.Optional[bool] = True, 

23 ): 

24 """Class initializer 

25 

26 Receives a settings dictionary and builds the object based on it. This 

27 dictionary is converted into a DotMap object to make it easier to access 

28 and modify the settings. 

29 

30 The settings dictionary should have the following structure: 

31 

32 .. code-block:: python 

33 

34 { 

35 'type': 'indices', # indices or data_streams 

36 'prefix': 'es-testbed', # The default prefix for everything we create 

37 'rollover_alias': False, # Only respected if 'type' == 'indices'. 

38 # Will rollover after creation and filling 1st 

39 # If True, will be overridden to value of alias 

40 # If False, will be overridden with None 

41 'uniq': 'my-unique-str', # If not provided, randomstr() 

42 'repository': # Only used for cold/frozen tier for snapshots 

43 'ilm': { # All of these ILM values are defaults 

44 'enabled': False, 

45 'phases': ['hot', 'delete'], 

46 'readonly': PHASE # Define readonly action during named PHASE 

47 'forcemerge': False, 

48 'max_num_segments': 1, 

49 'policy': {} # Define full ILM policy in advance. 

50 }, 

51 'index_buildlist': [], # List of indices to create 

52 } 

53 

54 The index_buildlist is a list of dictionaries, each with a similar structure: 

55 

56 .. code-block:: python 

57 

58 'index_buildlist': [ 

59 { 

60 'preset': 'NAME', # docgen_preset name, included or otherwise 

61 'options': { # kwargs for the generator function 

62 'docs': 10, 

63 'start_at': 0, 

64 'match': True, 

65 } 

66 'target_tier': 'frozen' # Target tier for 1st (oldest) index created 

67 }, 

68 { 

69 'preset': 'NAME', # docgen_preset name, included or otherwise 

70 'options': { # kwargs for the generator function 

71 'docs': 10, 

72 'start_at': 10, 

73 'match': True, 

74 } 

75 'target_tier': 'cold' # Target tier for 2nd index created 

76 }, 

77 { 

78 'preset': 'NAME', # docgen_preset name, included or otherwise 

79 'options': { # kwargs for the generator function 

80 'docs': 10, 

81 'start_at': 20, 

82 'match': True, 

83 } 

84 'target_tier': 'hot' # Target tier for last (newest) index created 

85 }, 

86 ] 

87 

88 The index_buildlist can be provided after the object is created, and is, 

89 in fact, if using a preset, which will provide the entire plan, including 

90 the index_buildlist, and the values that need to go in the indices. 

91 

92 Args: 

93 settings (t.Dict): The settings dictionary 

94 autobuild (t.Optional[bool], optional): Whether to autobuild the plan. 

95 Defaults to True. 

96 

97 Raises: 

98 ValueError: Must provide a settings dictionary 

99 """ 

100 debug.lv2('Initializing PlanBuilder object...') 

101 if settings is None: 

102 raise ValueError('Must provide a settings dictionary') 

103 self.settings = settings 

104 debug.lv5(f'SETTINGS: {settings}') 

105 self._plan = DotMap(TESTPLAN) 

106 debug.lv5(f'INITIAL PLAN: {prettystr(self._plan)}') 

107 self._plan.cleanup = 'UNSET' # Future use? 

108 if autobuild: 

109 self.setup() 

110 debug.lv3('PlanBuilder object initialized') 

111 

112 @property 

113 def plan(self) -> DotMap: 

114 """Return the Plan""" 

115 return self._plan 

116 

117 @begin_end() 

118 def _create_lists(self) -> None: 

119 names = [ 

120 'indices', 

121 'data_stream', 

122 'snapshots', 

123 'ilm_policies', 

124 'index_templates', 

125 'component_templates', 

126 ] 

127 for name in names: 

128 self._plan[name] = [] 

129 

130 @begin_end() 

131 def setup(self) -> None: 

132 """Do initial setup of the Plan DotMap""" 

133 self._plan.uniq = randomstr(length=8, lowercase=True) 

134 self._create_lists() 

135 self.update(self.settings) # Override with settings. 

136 self.update_rollover_alias() 

137 debug.lv3('Rollover alias updated') 

138 self.update_ilm() 

139 debug.lv5(f'FINAL PLAN: {prettystr(self._plan.toDict())}') 

140 

141 @begin_end() 

142 def update(self, settings: t.Dict) -> None: 

143 """Update the Plan DotMap""" 

144 self._plan.update(DotMap(settings)) 

145 debug.lv5(f'Updated plan: {prettystr(self._plan.toDict())}') 

146 

147 @begin_end() 

148 def update_ilm(self) -> None: 

149 """Update the ILM portion of the Plan DotMap""" 

150 setdefault = False 

151 if 'ilm' not in self._plan: 

152 debug.lv3('key "ilm" is not in plan') 

153 setdefault = True 

154 if isinstance(self._plan.ilm, dict): 

155 _ = DotMap(self._plan.ilm) 

156 self._plan.ilm = _ 

157 if isinstance(self._plan.ilm, DotMap): 

158 if 'enabled' not in self._plan.ilm: 

159 # Override with defaults 

160 debug.lv3( 

161 'plan.ilm does not have key "enabled". Overriding with defaults' 

162 ) 

163 setdefault = True 

164 elif isinstance(self._plan.ilm, bool): 

165 if self._plan.ilm: 

166 logger.warning( 

167 '"plan.ilm: True" is incorrect. Use plan.ilm.enabled: True' 

168 ) 

169 debug.lv3('plan.ilm is boolean. Overriding with defaults') 

170 setdefault = True 

171 if setdefault: 

172 debug.lv3('Setting defaults for ILM') 

173 self._plan.ilm = DotMap(TESTPLAN['ilm']) 

174 if self._plan.ilm.enabled: 

175 ilm = self._plan.ilm 

176 if not isinstance(self._plan.ilm.phases, list): 

177 logger.error('Phases is not a list!') 

178 self._plan.ilm.phases = TESTPLAN['ilm']['phases'] 

179 for entity in self._plan.index_buildlist: 

180 if 'searchable' in entity and entity['searchable'] is not None: 

181 if not entity['searchable'] in ilm.phases: 

182 ilm.phases.append(entity['searchable']) 

183 debug.lv5(f'ILM = {ilm}') 

184 debug.lv5(f'self._plan.ilm = {self._plan.ilm}') 

185 kwargs = { 

186 'phases': ilm.phases, 

187 'forcemerge': ilm.forcemerge, 

188 'max_num_segments': ilm.max_num_segments, 

189 'readonly': ilm.readonly, 

190 'repository': self._plan.repository, 

191 } 

192 debug.lv5(f'KWARGS = {kwargs}') 

193 self._plan.ilm.policy = build_ilm_policy(**kwargs) 

194 

195 @begin_end() 

196 def update_rollover_alias(self) -> None: 

197 """Update the Rollover Alias value""" 

198 if self._plan.rollover_alias: 

199 self._plan.rollover_alias = f'{self._plan.prefix}-idx-{self._plan.uniq}' 

200 else: 

201 self._plan.rollover_alias = None 

202 debug.lv5(f'Updated rollover_alias = {self._plan.rollover_alias}')