Skip to content

Commit ebdb10a

Browse files
authored
Add host group support to console ssh command (#2135)
When using the console command with type ssh (default), the host argument can now also be an Ansible inventory group name. If the group contains a single host, it connects directly. If the group contains multiple hosts, an interactive selection list is displayed. AI-assisted: Claude Code Signed-off-by: Christian Berendt <berendt@osism.tech>
1 parent 52e05a1 commit ebdb10a

1 file changed

Lines changed: 72 additions & 1 deletion

File tree

osism/commands/console.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# SPDX-License-Identifier: Apache-2.0
22

3+
import json
34
import socket
45
import subprocess
56
from typing import Optional
@@ -9,6 +10,7 @@
910
from prompt_toolkit import prompt
1011

1112
from osism import settings, utils
13+
from osism.utils.inventory import get_hosts_from_inventory, get_inventory_path
1214
from 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"\nGroup 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+
94154
class 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

Comments
 (0)