11# SPDX-License-Identifier: Apache-2.0
22
3+ import json
34import socket
45import subprocess
56from typing import Optional
910from prompt_toolkit import prompt
1011
1112from osism import settings , utils
13+ from osism .utils .inventory import get_hosts_from_inventory , get_inventory_path
1214from osism .utils .ssh import ensure_known_hosts_file , KNOWN_HOSTS_PATH
1315
1416
@@ -91,6 +93,64 @@ def resolve_host_with_fallback(hostname: str) -> str:
9193 return hostname
9294
9395
96+ def get_hosts_from_group (group : str ) -> list :
97+ """Resolve an Ansible inventory group to its list of hosts.
98+
99+ Args:
100+ group: The inventory group name to resolve
101+
102+ Returns:
103+ Sorted list of hostnames in the group, or empty list if the
104+ group does not exist or cannot be resolved.
105+ """
106+ try :
107+ inventory_path = get_inventory_path ("/ansible/inventory/hosts.yml" )
108+ result = subprocess .check_output (
109+ [
110+ "ansible-inventory" ,
111+ "-i" ,
112+ inventory_path ,
113+ "--list" ,
114+ "--limit" ,
115+ group ,
116+ ],
117+ stderr = subprocess .DEVNULL ,
118+ )
119+ inventory = json .loads (result )
120+ hosts = get_hosts_from_inventory (inventory )
121+ return sorted (hosts )
122+ except Exception :
123+ logger .debug ("Could not resolve group %r" , group , exc_info = True )
124+ return []
125+
126+
127+ def select_host_from_list (hosts : list ) -> Optional [str ]:
128+ """Display a numbered list of hosts and let the user choose one.
129+
130+ Args:
131+ hosts: List of hostnames to choose from
132+
133+ Returns:
134+ The selected hostname, or None if the selection was cancelled.
135+ """
136+ print (f"\n Group contains { len (hosts )} hosts:\n " )
137+ for i , host in enumerate (hosts , 1 ):
138+ print (f" { i } ) { host } " )
139+ print ()
140+
141+ while True :
142+ answer = prompt ("Select host [1-{}]: " .format (len (hosts )))
143+ if answer .strip ().lower () in ("q" , "quit" , "exit" ):
144+ return None
145+ try :
146+ index = int (answer .strip ())
147+ if 1 <= index <= len (hosts ):
148+ return hosts [index - 1 ]
149+ except ValueError :
150+ pass
151+ print (f"Please enter a number between 1 and { len (hosts )} , or 'q' to cancel." )
152+
153+
94154class Run (Command ):
95155 def get_parser (self , prog_name ):
96156 parser = super (Run , self ).get_parser (prog_name )
@@ -104,7 +164,7 @@ def get_parser(self, prog_name):
104164 "host" ,
105165 nargs = 1 ,
106166 type = str ,
107- help = "Hostname or address of the console to connect" ,
167+ help = "Hostname, address, or inventory group of the console to connect" ,
108168 )
109169 return parser
110170
@@ -146,6 +206,17 @@ def take_action(self, parsed_args):
146206 shell = True ,
147207 )
148208 elif type_console == "ssh" :
209+ # Try to resolve as an inventory group
210+ group_hosts = get_hosts_from_group (host )
211+ if len (group_hosts ) == 1 :
212+ logger .info (f"Group '{ host } ' contains one host: { group_hosts [0 ]} " )
213+ host = group_hosts [0 ]
214+ elif len (group_hosts ) > 1 :
215+ selected = select_host_from_list (group_hosts )
216+ if not selected :
217+ return
218+ host = selected
219+
149220 # Resolve hostname with Netbox fallback
150221 resolved_host = resolve_host_with_fallback (host )
151222 # FIXME: use paramiko or something else more Pythonic + make operator user + key configurable
0 commit comments