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-16 12:23 -0600
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-16 12:23 -0600
1"""Functions that make Elasticsearch API Calls"""
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)
26if t.TYPE_CHECKING:
27 from elasticsearch8 import Elasticsearch
29es_wait_debug.level = debug.level
30# Set the debug level for es_wait to match the current debug level
32PAUSE_VALUE = float(getenv(PAUSE_ENVVAR, default=PAUSE_DEFAULT))
34logger = logging.getLogger(__name__)
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]
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
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
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 )
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
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
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)}')
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
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)
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))
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
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
343 :param client: ES client
344 :param name: Index name
345 :param doc_generator: The generator function
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)
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
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)
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
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
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
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
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
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
503@begin_end()
504def get_write_index(client: 'Elasticsearch', name: str) -> str:
505 """
506 Calls :py:meth:`~.elasticsearch.client.IndicesClient.get_alias`
508 :param client: A client connection object
509 :param name: An alias name
511 :type client: :py:class:`~.elasticsearch.Elasticsearch`
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
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
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,))
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 )
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 )
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
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
638 Because you can pass search patterns and aliases as name, each element comes back
639 as an array:
641 {'indices': [], 'aliases': [], 'data_streams': []}
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 _
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}')
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