@@ -277,6 +277,42 @@ def __init__(self, inventory_file: Optional[str] = None):
277
277
self ._host_cache : dict [str , Optional [testinfra .host .Host ]] = {}
278
278
super ().__init__ ()
279
279
280
+ def get_hosts_by_ansible (self , host_expression : str ) -> list [str ]:
281
+ """Evaluate ansible host expression to get host list.
282
+
283
+ Example of such expression:
284
+
285
+ 'foo,&bar,!baz[2:],foobar[-3:-4],foofoo-*,~someth.+'
286
+
287
+ See https://docs.ansible.com/ansible/latest/inventory_guide/intro_patterns.html#common-patterns
288
+ """
289
+ from ansible .inventory .manager import InventoryManager
290
+ from ansible .parsing .dataloader import DataLoader
291
+
292
+ # We can't support 'group[1:2]', expressions 'as is',
293
+ # because urllib from python 3.13+ rejects hostnames with invalid
294
+ # IPv6 addresses (in square brakets)
295
+ # E ValueError: Invalid IPv6 URL
296
+ # We ask user to use round brakets in testinfra 'URL', and
297
+ # replace it back to ansible-compatible expression here.
298
+ host_expression = host_expression .replace ("(" , "[" )
299
+ host_expression = host_expression .replace (")" , "]" )
300
+ if self .inventory_file :
301
+ sources = [self .inventory_file ]
302
+ else :
303
+ # we search for other options only if inventory is not passed
304
+ # explicitely.
305
+ # Inside ansible, 'ANSIBLE_INVENTORY' is 'DEFAULT_HOST_LIST'
306
+ # We respect both ANSIBLE_INVENTORY env var and ansible.cfg
307
+ from ansible .config .manager import ConfigManager
308
+
309
+ sources = ConfigManager ().get_config_value ("DEFAULT_HOST_LIST" )
310
+
311
+ loader = DataLoader ()
312
+ inv = InventoryManager (loader = loader , sources = sources )
313
+ hosts = [h .name for h in inv .list_hosts (host_expression )]
314
+ return list (hosts )
315
+
280
316
def get_hosts (self , pattern : str = "all" ) -> list [str ]:
281
317
inventory = self .inventory
282
318
result = set ()
@@ -290,13 +326,22 @@ def get_hosts(self, pattern: str = "all") -> list[str]:
290
326
"only implicit localhost is available"
291
327
)
292
328
else :
293
- for group in inventory :
294
- groupmatch = fnmatch .fnmatch (group , pattern )
295
- if groupmatch :
296
- result |= set (itergroup (inventory , group ))
297
- for host in inventory [group ].get ("hosts" , []):
298
- if fnmatch .fnmatch (host , pattern ):
299
- result .add (host )
329
+ if "~" in pattern :
330
+ raise ValueError (
331
+ "Regular expressions are not supported in host expression. "
332
+ "Found '~' in the host expression."
333
+ )
334
+ special_char_list = "!&,:()" # signs of host expression
335
+ if any (ch in special_char_list for ch in pattern ):
336
+ result = result .union (self .get_hosts_by_ansible (pattern ))
337
+ else :
338
+ for group in inventory :
339
+ groupmatch = fnmatch .fnmatch (group , pattern )
340
+ if groupmatch :
341
+ result |= set (itergroup (inventory , group ))
342
+ for host in inventory [group ].get ("hosts" , []):
343
+ if fnmatch .fnmatch (host , pattern ):
344
+ result .add (host )
300
345
return sorted (result )
301
346
302
347
@functools .cached_property
0 commit comments