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
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-21 21:08 -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