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
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 22:23 -0600
1"""TestPlan Class Definition"""
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
10logger = logging.getLogger(__name__)
13class PlanBuilder:
14 """
15 Plan builder class
17 """
19 def __init__(
20 self,
21 settings: t.Dict,
22 autobuild: t.Optional[bool] = True,
23 ):
24 """Class initializer
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.
30 The settings dictionary should have the following structure:
32 .. code-block:: python
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 }
54 The index_buildlist is a list of dictionaries, each with a similar structure:
56 .. code-block:: python
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 ]
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.
92 Args:
93 settings (t.Dict): The settings dictionary
94 autobuild (t.Optional[bool], optional): Whether to autobuild the plan.
95 Defaults to True.
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')
112 @property
113 def plan(self) -> DotMap:
114 """Return the Plan"""
115 return self._plan
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] = []
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())}')
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())}')
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)
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}')