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-17 22:23 -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