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

375 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-21 21:08 -0600

1"""Functions that make Elasticsearch API Calls""" 

2 

3# pylint: disable=R0913,R0917,W0707 

4import typing as t 

5import logging 

6from os import getenv 

7from elasticsearch8.exceptions import NotFoundError, TransportError 

8from es_wait import Exists, Snapshot 

9from es_wait import debug as es_wait_debug 

10from es_wait.exceptions import EsWaitFatal, EsWaitTimeout 

11from .debug import debug, begin_end 

12from .defaults import PAUSE_DEFAULT, PAUSE_ENVVAR # MAPPING 

13from .exceptions import ( 

14 NameChanged, 

15 ResultNotExpected, 

16 TestbedFailure, 

17 TestbedMisconfig, 

18) 

19from .utils import ( 

20 get_routing, 

21 mounted_name, 

22 prettystr, 

23 storage_type, 

24) 

25 

26if t.TYPE_CHECKING: 

27 from elasticsearch8 import Elasticsearch 

28 

29es_wait_debug.level = debug.level 

30# Set the debug level for es_wait to match the current debug level 

31 

32PAUSE_VALUE = float(getenv(PAUSE_ENVVAR, default=PAUSE_DEFAULT)) 

33 

34logger = logging.getLogger(__name__) 

35 

36 

37def emap(kind: str, es: "Elasticsearch", value=None) -> t.Dict[str, t.Any]: 

38 """Return a value from a dictionary""" 

39 _ = { 

40 "alias": { 

41 "delete": es.indices.delete_alias, 

42 "exists": es.indices.exists_alias, 

43 "get": es.indices.get_alias, 

44 "kwargs": {"index": value, "expand_wildcards": ["open", "closed"]}, 

45 "plural": "alias(es)", 

46 }, 

47 "data_stream": { 

48 "delete": es.indices.delete_data_stream, 

49 "exists": es.indices.exists, 

50 "get": es.indices.get_data_stream, 

51 "kwargs": {"name": value, "expand_wildcards": ["open", "closed"]}, 

52 "plural": "data_stream(s)", 

53 "key": "data_streams", 

54 }, 

55 "index": { 

56 "delete": es.indices.delete, 

57 "exists": es.indices.exists, 

58 "get": es.indices.get, 

59 "kwargs": {"index": value, "expand_wildcards": ["open", "closed"]}, 

60 "plural": "index(es)", 

61 }, 

62 "template": { 

63 "delete": es.indices.delete_index_template, 

64 "exists": es.indices.exists_index_template, 

65 "get": es.indices.get_index_template, 

66 "kwargs": {"name": value}, 

67 "plural": "index template(s)", 

68 "key": "index_templates", 

69 }, 

70 "ilm": { 

71 "delete": es.ilm.delete_lifecycle, 

72 "exists": es.ilm.get_lifecycle, 

73 "get": es.ilm.get_lifecycle, 

74 "kwargs": {"name": value}, 

75 "plural": "ilm policy(ies)", 

76 }, 

77 "component": { 

78 "delete": es.cluster.delete_component_template, 

79 "exists": es.cluster.exists_component_template, 

80 "get": es.cluster.get_component_template, 

81 "kwargs": {"name": value}, 

82 "plural": "component template(s)", 

83 "key": "component_templates", 

84 }, 

85 "snapshot": { 

86 "delete": es.snapshot.delete, 

87 "exists": es.snapshot.get, 

88 "get": es.snapshot.get, 

89 "kwargs": {"snapshot": value}, 

90 "plural": "snapshot(s)", 

91 }, 

92 } 

93 return _[kind] 

94 

95 

96@begin_end() 

97def change_ds(client: "Elasticsearch", actions: t.Optional[str] = None) -> None: 

98 """Change/Modify/Update a data_stream""" 

99 try: 

100 debug.lv4("TRY: client.indices.modify_data_stream") 

101 debug.lv5(f"modify_data_stream actions: {actions}") 

102 res = client.indices.modify_data_stream(actions=actions, body=None) 

103 debug.lv5(f"modify_data_stream response: {res}") 

104 except Exception as err: 

105 debug.lv3("Exiting function, raising exception") 

106 debug.lv5(f"Exception: {prettystr(err)}") 

107 raise ResultNotExpected( 

108 f"Unable to modify data_streams. {prettystr(err)}" 

109 ) from err 

110 

111 

112@begin_end() 

113def wait_wrapper( 

114 client: "Elasticsearch", 

115 wait_cls: t.Callable, 

116 wait_kwargs: t.Dict, 

117 func: t.Callable, 

118 f_kwargs: t.Dict, 

119) -> None: 

120 """Wrapper function for waiting on an object to be created""" 

121 try: 

122 debug.lv4("TRY: func()") 

123 debug.lv5(f"func kwargs: {f_kwargs}") 

124 func(**f_kwargs) 

125 debug.lv4("TRY: wait_cls") 

126 debug.lv5(f"wait_cls kwargs: {wait_kwargs}") 

127 test = wait_cls(client, **wait_kwargs) 

128 debug.lv4("TRY: wait()") 

129 test.wait() 

130 except EsWaitFatal as wait: 

131 msg = f"{wait.message}. Elapsed time: {wait.elapsed}. Errors: {wait.errors}" 

132 debug.lv3("Exiting function, raising exception") 

133 debug.lv5(f"Exception: {prettystr(wait)}") 

134 raise TestbedFailure(msg) from wait 

135 except EsWaitTimeout as wait: 

136 msg = f"{wait.message}. Elapsed time: {wait.elapsed}. Timeout: {wait.timeout}" 

137 debug.lv3("Exiting function, raising exception") 

138 debug.lv5(f"Exception: {prettystr(wait)}") 

139 raise TestbedFailure(msg) from wait 

140 except TransportError as err: 

141 debug.lv3("Exiting function, raising exception") 

142 debug.lv5(f"Exception: {prettystr(err)}") 

143 raise TestbedFailure( 

144 f"Elasticsearch TransportError class exception encountered:" 

145 f"{prettystr(err)}" 

146 ) from err 

147 except Exception as err: 

148 debug.lv3("Exiting function, raising exception") 

149 debug.lv5(f"Exception: {prettystr(err)}") 

150 raise TestbedFailure(f"General Exception caught: {prettystr(err)}") from err 

151 

152 

153@begin_end() 

154def create_data_stream(client: "Elasticsearch", name: str) -> None: 

155 """Create a data_stream""" 

156 wait_kwargs = {"name": name, "kind": "data_stream", "pause": PAUSE_VALUE} 

157 debug.lv5(f"wait_kwargs: {wait_kwargs}") 

158 f_kwargs = {"name": name} 

159 debug.lv5(f"f_kwargs: {f_kwargs}") 

160 debug.lv5(f"Creating data_stream {name} and waiting for it to exist") 

161 wait_wrapper( 

162 client, Exists, wait_kwargs, client.indices.create_data_stream, f_kwargs 

163 ) 

164 

165 

166@begin_end() 

167def create_index( 

168 client: "Elasticsearch", 

169 name: str, 

170 tier: str = "hot", 

171 aliases: t.Optional[t.Dict] = None, 

172 mappings: t.Optional[t.Dict] = None, 

173 settings: t.Optional[t.Dict] = None, 

174) -> None: 

175 """Create named index""" 

176 if not settings: 

177 settings = get_routing(tier=tier) 

178 else: 

179 settings.update(get_routing(tier=tier)) 

180 debug.lv5(f"settings: {settings}") 

181 wait_kwargs = {"name": name, "kind": "index", "pause": PAUSE_VALUE} 

182 debug.lv5(f"wait_kwargs: {wait_kwargs}") 

183 f_kwargs = { 

184 "index": name, 

185 "aliases": aliases, 

186 "mappings": mappings, 

187 "settings": settings, 

188 } 

189 debug.lv5(f"f_kwargs: {f_kwargs}") 

190 debug.lv5(f"Creating index {name} and waiting for it to exist") 

191 wait_wrapper(client, Exists, wait_kwargs, client.indices.create, f_kwargs) 

192 retval = exists(client, "index", name) 

193 debug.lv5(f"Return value = {retval}") 

194 return retval 

195 

196 

197@begin_end() 

198def verify( 

199 client: "Elasticsearch", 

200 kind: str, 

201 name: str, 

202 repository: t.Optional[str] = None, 

203) -> bool: 

204 """Verify that whatever was deleted is actually deleted""" 

205 success = True 

206 items = name.split(",") 

207 for item in items: 

208 result = exists(client, kind, item, repository=repository) 

209 if result: # That means it's still in the cluster 

210 success = False 

211 debug.lv5(f"Return value = {success}") 

212 return success 

213 

214 

215@begin_end() 

216def delete( 

217 client: "Elasticsearch", 

218 kind: str, 

219 name: str, 

220 repository: t.Optional[str] = None, 

221) -> bool: 

222 """Delete the named object of type kind""" 

223 which = emap(kind, client) 

224 func = which["delete"] 

225 success = False 

226 if name is not None: # Typically only with ilm 

227 try: 

228 debug.lv4("TRY: func") 

229 if kind == "snapshot": 

230 debug.lv5(f"Deleting snapshot {name} from repository {repository}") 

231 res = func(snapshot=name, repository=repository) 

232 elif kind == "index": 

233 debug.lv5(f"Deleting index {name}") 

234 res = func(index=name) 

235 else: 

236 debug.lv5(f"Deleting {kind} {name}") 

237 res = func(name=name) 

238 except NotFoundError as err: 

239 debug.lv5(f"{kind} named {name} not found: {prettystr(err)}") 

240 

241 debug.lv5("Value = True") 

242 return True 

243 except Exception as err: 

244 debug.lv3("Exiting function, raising exception") 

245 debug.lv5(f"Exception: {prettystr(err)}") 

246 raise ResultNotExpected(f"Unexpected result: {prettystr(err)}") from err 

247 if "acknowledged" in res and res["acknowledged"]: 

248 success = True 

249 debug.lv3(f'Deleted {which["plural"]}: "{name}"') 

250 else: 

251 debug.lv5("Verifying deletion manually") 

252 success = verify(client, kind, name, repository=repository) 

253 else: 

254 debug.lv3(f'"{kind}" has a None value for name') 

255 debug.lv5(f"Return value = {success}") 

256 return success 

257 

258 

259@begin_end() 

260def do_snap( 

261 client: "Elasticsearch", repo: str, snap: str, idx: str, tier: str = "cold" 

262) -> None: 

263 """Perform a snapshot""" 

264 wait_kwargs = {"snapshot": snap, "repository": repo, "pause": 1, "timeout": 60} 

265 debug.lv5(f"wait_kwargs: {wait_kwargs}") 

266 f_kwargs = {"repository": repo, "snapshot": snap, "indices": idx} 

267 debug.lv5(f"f_kwargs: {f_kwargs}") 

268 debug.lv5(f"Creating snapshot {snap} and waiting for it to complete") 

269 wait_wrapper(client, Snapshot, wait_kwargs, client.snapshot.create, f_kwargs) 

270 

271 # Mount the index accordingly 

272 debug.lv5( 

273 f"Mounting index {idx} from snapshot {snap} as searchable snapshot " 

274 f"with mounted name: {mounted_name(idx, tier)}" 

275 ) 

276 client.searchable_snapshots.mount( 

277 repository=repo, 

278 snapshot=snap, 

279 index=idx, 

280 index_settings=get_routing(tier=tier), 

281 renamed_index=mounted_name(idx, tier), 

282 storage=storage_type(tier), 

283 wait_for_completion=True, 

284 ) 

285 # Fix aliases 

286 debug.lv5(f"Fixing aliases for {idx} to point to {mounted_name(idx, tier)}") 

287 fix_aliases(client, idx, mounted_name(idx, tier)) 

288 

289 

290@begin_end() 

291def exists( 

292 client: "Elasticsearch", kind: str, name: str, repository: t.Union[str, None] = None 

293) -> bool: 

294 """Return boolean existence of the named kind of object""" 

295 if name is None: 

296 return False 

297 retval = True 

298 func = emap(kind, client)["exists"] 

299 try: 

300 debug.lv4("TRY: func") 

301 if kind == "snapshot": 

302 # Expected response: {'snapshots': [{'snapshot': name, ...}]} 

303 # Since we are specifying by name, there should only be one returned 

304 debug.lv5(f"Checking for snapshot {name} in repository {repository}") 

305 res = func(snapshot=name, repository=repository) 

306 debug.lv3(f"Snapshot response: {res}") 

307 # If there are no entries, load a default None value for the check 

308 _ = dict(res["snapshots"][0]) if res else {"snapshot": None} 

309 # Since there should be only 1 snapshot with this name, we can check it 

310 retval = bool(_["snapshot"] == name) 

311 elif kind == "ilm": 

312 # There is no true 'exists' method for ILM, so we have to get the policy 

313 # and check for a NotFoundError 

314 debug.lv5(f"Checking for ILM policy {name}") 

315 retval = bool(name in dict(func(name=name))) 

316 elif kind in ["index", "data_stream"]: 

317 debug.lv5(f"Checking for {kind} {name}") 

318 retval = func(index=name) 

319 else: 

320 debug.lv5(f"Checking for {kind} {name}") 

321 retval = func(name=name) 

322 except NotFoundError: 

323 debug.lv5(f"{kind} named {name} not found") 

324 retval = False 

325 except Exception as err: 

326 debug.lv3("Exiting function, raising exception") 

327 debug.lv5(f"Exception: {prettystr(err)}") 

328 raise ResultNotExpected(f"Unexpected result: {prettystr(err)}") from err 

329 debug.lv5(f"Return value = {retval}") 

330 return retval 

331 

332 

333@begin_end() 

334def fill_index( 

335 client: "Elasticsearch", 

336 name: t.Optional[str] = None, 

337 doc_generator: t.Optional[t.Generator[t.Dict, None, None]] = None, 

338 options: t.Optional[t.Dict] = None, 

339) -> None: 

340 """ 

341 Create and fill the named index with mappings and settings as directed 

342 

343 :param client: ES client 

344 :param name: Index name 

345 :param doc_generator: The generator function 

346 

347 :returns: No return value 

348 """ 

349 if not options: 

350 options = {} 

351 for doc in doc_generator(**options): 

352 client.index(index=name, document=doc) 

353 client.indices.flush(index=name) 

354 client.indices.refresh(index=name) 

355 

356 

357@begin_end() 

358def find_write_index(client: "Elasticsearch", name: str) -> t.AnyStr: 

359 """Find the write_index for an alias by searching any index the alias points to""" 

360 retval = None 

361 for alias in get_aliases(client, name): 

362 debug.lv5(f"Inspecting alias: {alias}") 

363 retval = get_write_index(client, alias) 

364 debug.lv5(f"find_write_index response: {retval}") 

365 if retval: 

366 debug.lv5(f"Found write index: {retval}") 

367 break 

368 debug.lv5(f"Return value = {retval}") 

369 return retval 

370 

371 

372@begin_end() 

373def fix_aliases(client: "Elasticsearch", oldidx: str, newidx: str) -> None: 

374 """Fix aliases using the new and old index names as data""" 

375 # Delete the original index 

376 debug.lv5(f"Deleting index {oldidx}") 

377 client.indices.delete(index=oldidx) 

378 # Add the original index name as an alias to the mounted index 

379 debug.lv5(f"Adding alias {oldidx} to index {newidx}") 

380 client.indices.put_alias(index=f"{newidx}", name=oldidx) 

381 

382 

383@begin_end() 

384def get( 

385 client: "Elasticsearch", 

386 kind: str, 

387 pattern: str, 

388 repository: t.Optional[str] = None, 

389) -> t.Sequence[str]: 

390 """get any/all objects of type kind matching pattern""" 

391 if pattern is None: 

392 msg = f'"{kind}" has a None value for pattern' 

393 logger.error(msg) 

394 raise TestbedMisconfig(msg) 

395 which = emap(kind, client, value=pattern) 

396 func = which["get"] 

397 kwargs = which["kwargs"] 

398 if kind == "snapshot": 

399 kwargs["repository"] = repository 

400 try: 

401 debug.lv4("TRY: func") 

402 debug.lv5(f"func kwargs: {kwargs}") 

403 result = func(**kwargs) 

404 except NotFoundError: 

405 debug.lv3(f'{kind} pattern "{pattern}" had zero matches') 

406 return [] 

407 except Exception as err: 

408 raise ResultNotExpected(f"Unexpected result: {prettystr(err)}") from err 

409 if kind == "snapshot": 

410 debug.lv5("Checking for snapshot") 

411 retval = [x["snapshot"] for x in result["snapshots"]] 

412 elif kind in ["data_stream", "template", "component"]: 

413 debug.lv5("Checking for data_stream/template/component") 

414 retval = [x["name"] for x in result[which["key"]]] 

415 else: 

416 debug.lv5("Checking for alias/ilm/index") 

417 retval = list(result.keys()) 

418 debug.lv5(f"Return value = {retval}") 

419 return retval 

420 

421 

422@begin_end() 

423def get_aliases(client: "Elasticsearch", name: str) -> t.Sequence[str]: 

424 """Get aliases from index 'name'""" 

425 res = client.indices.get(index=name) 

426 debug.lv5(f"get_aliases response: {res}") 

427 try: 

428 debug.lv4("TRY: getting aliases") 

429 retval = list(res[name]["aliases"].keys()) 

430 debug.lv5(f"list(res[name]['aliases'].keys()) = {retval}") 

431 except KeyError: 

432 retval = None 

433 debug.lv5(f"Return value = {retval}") 

434 return retval 

435 

436 

437@begin_end() 

438def get_backing_indices(client: "Elasticsearch", name: str) -> t.Sequence[str]: 

439 """Get the backing indices from the named data_stream""" 

440 resp = resolver(client, name) 

441 data_streams = resp["data_streams"] 

442 retval = [] 

443 if data_streams: 

444 debug.lv5("Checking for backing indices...") 

445 if len(data_streams) > 1: 

446 debug.lv3("Exiting function, raising exception") 

447 debug.lv5(f"ResultNotExpected: More than 1 found {data_streams}") 

448 raise ResultNotExpected( 

449 f"Expected only a single data_stream matching {name}" 

450 ) 

451 retval = data_streams[0]["backing_indices"] 

452 debug.lv5(f"Return value = {retval}") 

453 return retval 

454 

455 

456@begin_end() 

457def get_ds_current(client: "Elasticsearch", name: str) -> str: 

458 """ 

459 Find which index is the current 'write' index of the data_stream 

460 This is best accomplished by grabbing the last backing_index 

461 """ 

462 backers = get_backing_indices(client, name) 

463 retval = None 

464 if backers: 

465 retval = backers[-1] 

466 debug.lv5(f"Return value = {retval}") 

467 return retval 

468 

469 

470@begin_end() 

471def get_ilm(client: "Elasticsearch", pattern: str) -> t.Union[t.Dict[str, str], None]: 

472 """Get any ILM entity in ES that matches pattern""" 

473 try: 

474 debug.lv4("TRY: ilm.get_lifecycle") 

475 retval = client.ilm.get_lifecycle(name=pattern) 

476 except Exception as err: 

477 msg = f"Unable to get ILM lifecycle matching {pattern}. Error: {prettystr(err)}" 

478 logger.critical(msg) 

479 debug.lv3("Exiting function, raising exception") 

480 debug.lv5(f"Exception: {prettystr(err)}") 

481 raise ResultNotExpected(msg) from err 

482 debug.lv5(f"Return value = {retval}") 

483 return retval 

484 

485 

486@begin_end() 

487def get_ilm_phases(client: "Elasticsearch", name: str) -> t.Dict: 

488 """Return the policy/phases part of the ILM policy identified by 'name'""" 

489 ilm = get_ilm(client, name) 

490 try: 

491 debug.lv4("TRY: get ILM phases") 

492 retval = ilm[name]["policy"]["phases"] 

493 except KeyError as err: 

494 msg = f"Unable to get ILM lifecycle named {name}. Error: {prettystr(err)}" 

495 logger.critical(msg) 

496 debug.lv3("Exiting function, raising exception") 

497 debug.lv5(f"Exception: {prettystr(err)}") 

498 raise ResultNotExpected(msg) from err 

499 debug.lv5(f"Return value = {retval}") 

500 return retval 

501 

502 

503@begin_end() 

504def get_write_index(client: "Elasticsearch", name: str) -> str: 

505 """ 

506 Calls :py:meth:`~.elasticsearch.client.IndicesClient.get_alias` 

507 

508 :param client: A client connection object 

509 :param name: An alias name 

510 

511 :type client: :py:class:`~.elasticsearch.Elasticsearch` 

512 

513 :returns: The the index name associated with the alias that is designated 

514 ``is_write_index`` 

515 """ 

516 response = client.indices.get_alias(index=name) 

517 debug.lv5(f"get_alias response: {response}") 

518 retval = None 

519 for index in list(response.keys()): 

520 try: 

521 debug.lv4("TRY: get write index") 

522 if response[index]["aliases"][name]["is_write_index"]: 

523 retval = index 

524 break 

525 except KeyError: 

526 continue 

527 debug.lv5(f"Return value = {retval}") 

528 return retval 

529 

530 

531@begin_end() 

532def ilm_explain(client: "Elasticsearch", name: str) -> t.Union[t.Dict, None]: 

533 """Return the results from the ILM Explain API call for the named index""" 

534 try: 

535 debug.lv4("TRY: ilm.explain_lifecycle") 

536 retval = client.ilm.explain_lifecycle(index=name)["indices"][name] 

537 except KeyError: 

538 debug.lv5("Index name changed") 

539 new = list(client.ilm.explain_lifecycle(index=name)["indices"].keys())[0] 

540 debug.lv5(f"ilm.explain_lifecycle response: {new}") 

541 retval = client.ilm.explain_lifecycle(index=new)["indices"][new] 

542 except NotFoundError as err: 

543 logger.warning(f"Datastream/Index Name changed. {name} was not found") 

544 debug.lv3("Exiting function, raising exception") 

545 debug.lv5(f"Exception: {prettystr(err)}") 

546 raise NameChanged(f"{name} was not found, likely due to a name change") from err 

547 except Exception as err: 

548 msg = f"Unable to get ILM information for index {name}" 

549 logger.critical(msg) 

550 debug.lv3("Exiting function, raising exception") 

551 debug.lv5(f"Exception: {prettystr(err)}") 

552 raise ResultNotExpected(f"{msg}. Exception: {prettystr(err)}") from err 

553 debug.lv5(f"Return value = {retval}") 

554 return retval 

555 

556 

557@begin_end() 

558def ilm_move( 

559 client: "Elasticsearch", name: str, current_step: t.Dict, next_step: t.Dict 

560) -> None: 

561 """Move index 'name' from the current step to the next step""" 

562 try: 

563 debug.lv4("TRY: ilm.move_to_step") 

564 res = client.ilm.move_to_step( 

565 index=name, current_step=current_step, next_step=next_step 

566 ) 

567 debug.lv5(f"ilm.move_to_step response: {res}") 

568 except Exception as err: 

569 msg = ( 

570 f"Unable to move index {name} to ILM next step: {next_step}. " 

571 f"Error: {prettystr(err)}" 

572 ) 

573 logger.critical(msg) 

574 raise ResultNotExpected(msg, (err,)) 

575 

576 

577@begin_end() 

578def put_comp_tmpl(client: "Elasticsearch", name: str, component: t.Dict) -> None: 

579 """Publish a component template""" 

580 wait_kwargs = {"name": name, "kind": "component_template", "pause": PAUSE_VALUE} 

581 f_kwargs = {"name": name, "template": component, "create": True} 

582 wait_wrapper( 

583 client, 

584 Exists, 

585 wait_kwargs, 

586 client.cluster.put_component_template, 

587 f_kwargs, 

588 ) 

589 

590 

591@begin_end() 

592def put_idx_tmpl( 

593 client: "Elasticsearch", 

594 name: str, 

595 index_patterns: t.List[str], 

596 components: t.List[str], 

597 data_stream: t.Optional[t.Dict] = None, 

598) -> None: 

599 """Publish an index template""" 

600 wait_kwargs = {"name": name, "kind": "index_template", "pause": PAUSE_VALUE} 

601 f_kwargs = { 

602 "name": name, 

603 "composed_of": components, 

604 "data_stream": data_stream, 

605 "index_patterns": index_patterns, 

606 "create": True, 

607 } 

608 wait_wrapper( 

609 client, 

610 Exists, 

611 wait_kwargs, 

612 client.indices.put_index_template, 

613 f_kwargs, 

614 ) 

615 

616 

617@begin_end() 

618def put_ilm( 

619 client: "Elasticsearch", name: str, policy: t.Union[t.Dict, None] = None 

620) -> None: 

621 """Publish an ILM Policy""" 

622 try: 

623 debug.lv4("TRY: ilm.put_lifecycle") 

624 debug.lv5(f"ilm.put_lifecycle name: {name}, policy: {policy}") 

625 res = client.ilm.put_lifecycle(name=name, policy=policy) 

626 debug.lv5(f"ilm.put_lifecycle response: {res}") 

627 except Exception as err: 

628 msg = f"Unable to put ILM policy {name}. Error: {prettystr(err)}" 

629 logger.error(msg) 

630 raise TestbedFailure(msg) from err 

631 

632 

633@begin_end() 

634def resolver(client: "Elasticsearch", name: str) -> dict: 

635 """ 

636 Resolve details about the entity, be it an index, alias, or data_stream 

637 

638 Because you can pass search patterns and aliases as name, each element comes back 

639 as an array: 

640 

641 {'indices': [], 'aliases': [], 'data_streams': []} 

642 

643 If you only resolve a single index or data stream, you will still have a 1-element 

644 list 

645 """ 

646 _ = client.indices.resolve_index(name=name, expand_wildcards=["open", "closed"]) 

647 debug.lv5(f"Return value = {_}") 

648 return _ 

649 

650 

651@begin_end() 

652def rollover(client: "Elasticsearch", name: str) -> None: 

653 """Rollover alias or data_stream identified by name""" 

654 res = client.indices.rollover(alias=name, wait_for_active_shards="all") 

655 debug.lv5(f"rollover response: {res}") 

656 

657 

658@begin_end() 

659def snapshot_name(client: "Elasticsearch", name: str) -> t.Union[t.AnyStr, None]: 

660 """Get the name of the snapshot behind the mounted index data""" 

661 res = {} 

662 if exists(client, "index", name): # Can jump straight to nested keys if it exists 

663 res = client.indices.get(index=name)[name]["settings"]["index"] 

664 debug.lv5(f"indices.get response: {res}") 

665 try: 

666 debug.lv4("TRY: retval = res['store']['snapshot']['snapshot_name']") 

667 retval = res["store"]["snapshot"]["snapshot_name"] 

668 except KeyError: 

669 logger.error(f"{name} is not a searchable snapshot") 

670 retval = None 

671 debug.lv5(f"Return value = {retval}") 

672 return retval