Skip to content

Commit 805ca08

Browse files
committed
refactor: introduce the SSH config converter
to docker-entrypoint.sh & README.md; * properly parse the SSH configuration applies to multiple hosts * cleanhouse * more validation
1 parent 0523bfe commit 805ca08

File tree

7 files changed

+277
-164
lines changed

7 files changed

+277
-164
lines changed

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ FROM gcr.io/distroless/python3:latest
99
COPY --link --chmod=0755 ./docker-entrypoint.sh /usr/local/bin/
1010
# toybox + bash(ash) + catatonit
1111
COPY --link --from=assets /usr/bin/ /usr/bin/
12+
13+
COPY --link --chmod=0755 ./sshconfig_to_ananta/ /home/nonroot/sshconfig_to_ananta/
1214
COPY --link --from=build /home/nonroot/.local/ /home/nonroot/.local/
1315

1416
SHELL ["/usr/bin/bash", "-o", "pipefail", "-c"]

README.md

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,32 @@
44
docker pull icecodexi/ananta:latest
55
mkdir -p "${HOME}/.ssh/"
66
find "${HOME}/.ssh/" -type f -print0 | xargs -0 -r chmod 600
7-
touch "$(pwd)/hosts.csv"
87

8+
_extra_args=()
99
if [[ "$UID" -eq '0' ]]; then
10-
_run_as_root='--user root'
10+
_extra_args+=('--user' 'root')
1111
fi
12-
docker run --rm --interactive --tty \
13-
${_run_as_root} \
14-
--volume /etc/localtime:/etc/localtime:ro \
15-
--volume "${HOME}/.ssh/:/home/nonroot/.ssh/:ro" \
16-
--volume "$(pwd)/hosts.csv:/home/nonroot/hosts.csv:ro" \
17-
--cpu-shares 512 --memory 512M --memory-swap 512M \
18-
--security-opt no-new-privileges \
19-
icecodexi/ananta:latest \
20-
--help
12+
13+
# Will automatic generate hosts.csv based on ~/.ssh/config if it does not exist
14+
_hosts_csv="$(pwd)/hosts.csv"
15+
if [[ -f "${_hosts_csv}" ]]; then
16+
hosts_csv="${_hosts_csv}"
17+
_extra_args+=('--volume' "${_hosts_csv}:/home/nonroot/hosts.csv:ro")
18+
fi
19+
20+
export _extra_args hosts_csv
21+
# put this function definition in your ~/.bashrc
22+
ananta() {
23+
docker run --rm --interactive --tty \
24+
"${_extra_args[@]}" \
25+
--volume /etc/localtime:/etc/localtime:ro \
26+
--volume "${HOME}/.ssh/:/home/nonroot/.ssh/:ro" \
27+
--cpu-shares 512 --memory 512M --memory-swap 512M \
28+
--security-opt no-new-privileges \
29+
icecodexi/ananta:latest "$hosts_csv" \
30+
"$@"
31+
}
32+
33+
## Example for issuing command `whoami` to multiple hosts
34+
ananta whoami
2135
```

docker-entrypoint.sh

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,48 @@
22
set -e -o pipefail
33

44
cd /home/nonroot/.ssh/ || exit 1
5+
ananta_args=()
6+
ssh_command=()
7+
hosts_file="/home/nonroot/_autogen_hosts.csv"
58

6-
args=()
79
while [[ $# -gt 0 ]]; do
810
case "$1" in
911
-[kK]|--default-key)
1012
_DEFAULT_KEY="$2"
1113
# Modify the key path to be under /home/nonroot/.ssh/
1214
key_file=$(basename "$_DEFAULT_KEY")
1315
# Add the modified parameter
14-
args+=("$1" "/home/nonroot/.ssh/$key_file")
16+
ananta_args+=("$1" "/home/nonroot/.ssh/$key_file")
1517
shift 2
1618
;;
1719
# Keep other parameters unchanged
1820
-[nNsSeEcCvV]|--no-color|--separate-output|--allow-empty-line|--allow-cursor-control|--version)
19-
args+=("$1")
21+
ananta_args+=("$1")
2022
shift
2123
;;
2224
-[tTwW]|--host-tags|--terminal-width)
23-
args+=("$1" "$2")
25+
ananta_args+=("$1" "$2")
2426
shift 2
2527
;;
26-
[!-]*)
28+
''|[!-]*)
2729
_HOSTS_CSV="$1"
28-
# Modify the hosts.csv path to be under /home/nonroot/
29-
hosts_file=$(basename "$_HOSTS_CSV")
30-
# Add the modified parameter
31-
args+=("/home/nonroot/$hosts_file")
3230
shift
3331
# Stop processing remaining arguments
34-
args+=("$@")
32+
ssh_command=("$@")
3533
break
3634
;;
3735
esac
3836
done
3937

38+
if [[ -n "$_HOSTS_CSV" ]]; then
39+
# Modify the hosts.csv path to be under /home/nonroot/
40+
hosts_file="/home/nonroot/$(basename "$_HOSTS_CSV")"
41+
else
42+
echo "INFO: Will try to generate one using the SSH config..."
43+
python3 /home/nonroot/sshconfig_to_ananta/main.py \
44+
--ssh /home/nonroot/.ssh/config \
45+
"$hosts_file"
46+
fi
47+
4048
# Run the ananta command with the modified arguments
41-
exec catatonit -- ananta "${args[@]}"
49+
exec catatonit -- ananta "${ananta_args[@]}" "$hosts_file" "${ssh_command[@]}"

sshconfig2ananta.py

Lines changed: 0 additions & 142 deletions
This file was deleted.

sshconfig_to_ananta/ananta_host.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env python3
2+
# pyright: strict
3+
4+
from ipaddress import IPv4Address, IPv6Address, ip_address
5+
from pathlib import Path
6+
from typing import List
7+
8+
9+
class AnantaHost:
10+
"""Represents an Ananta host entry.
11+
12+
This class stores the configuration details for connecting to a host
13+
via SSH, intended for use with the Ananta system.
14+
15+
The expected format for representing a host is a comma-separated string:
16+
alias,ip,port,username,key_path[,tags]
17+
18+
Where:
19+
- alias: A short name or identifier for the host.
20+
- ip: The IP address or hostname of the host.
21+
- port: The SSH port number (defaults to 22 if empty or omitted).
22+
- username: The username for SSH login (defaults to 'root' if empty or omitted).
23+
- key_path: The path to the SSH private key file. Use '#' if not applicable or managed elsewhere.
24+
- tags (optional): Colon-separated tags for categorization (e.g., 'web', 'db', 'arch:web').
25+
26+
Examples:
27+
- host-1,10.0.0.1,22,user,/home/user/.ssh/id_ed25519
28+
- host-2,10.0.0.2,22,user,#,web
29+
- host-3,10.0.0.3,22,user,#,arch:web
30+
- host-4,10.0.0.4,22,user,#,ubuntu:db
31+
"""
32+
33+
alias: str
34+
ip: IPv4Address | IPv6Address
35+
port: int
36+
username: str
37+
key_path: str
38+
tags: List[str]
39+
40+
def __init__(
41+
self,
42+
alias: str,
43+
ip: str,
44+
port: str,
45+
username: str,
46+
key_path: str,
47+
tags: List[str],
48+
):
49+
if not alias:
50+
raise ValueError("ERROR: alias cannot be empty.")
51+
52+
try:
53+
self.ip = ip_address(ip)
54+
except ValueError as e:
55+
raise ValueError(f"ERROR: Invalid IP address: {ip}") from e
56+
57+
try:
58+
self.port = int(port) if str(port) else 22
59+
if not (0 < self.port < 65536):
60+
raise ValueError(f"ERROR: Port number {port} must be greater than 0 and less than 65536.")
61+
except ValueError as e:
62+
raise ValueError(f"ERROR: Invalid port number: {port}") from e
63+
64+
self.username = username if username else "root"
65+
66+
self.key_path = key_path if key_path else "#"
67+
if not Path(key_path).is_file():
68+
raise FileNotFoundError(f"ERROR: SSH Key {key_path} could not be found OR is not a regular file.")
69+
70+
self.tags = tags if tags else []
71+
72+
def to_string_with_feilds(self) -> str:
73+
return (
74+
f" alias: {self.alias}\n"
75+
f" ip: {self.ip}\n"
76+
f" port: {self.port}\n"
77+
f" username: {self.username}\n"
78+
f" key_path: {self.key_path}\n"
79+
f" tags: {self.tags}\n"
80+
)
81+
82+
def to_string(self) -> str:
83+
parts = [self.alias, str(self.ip), str(self.port), self.username, self.key_path]
84+
if self.tags:
85+
parts.append(":".join(self.tags))
86+
return ",".join(parts) + "\n"

sshconfig_to_ananta/main.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env python3
2+
# pyright: strict
3+
4+
import argparse
5+
from pathlib import Path
6+
from ssh_config_converter import convert_to_ananta_hosts
7+
8+
def parse_arguments():
9+
parser = argparse.ArgumentParser(description="Convert SSH config to Ananta hosts csv.")
10+
parser.add_argument("--ssh", help="SSH config file.", default=Path.home() / ".ssh" / "config")
11+
parser.add_argument("csvfile", help="Path the Ananta hosts file would be written to. (better set to a path that can safely overwrite)")
12+
return parser.parse_args()
13+
14+
15+
if __name__ == "__main__":
16+
args = parse_arguments()
17+
ssh_path = Path(args.ssh)
18+
csvfile = Path(args.csvfile)
19+
20+
if csvfile.is_dir() or csvfile.is_symlink():
21+
raise IsADirectoryError(f"ERROR: {csvfile} is a directory OR a symlink. Script aborted to prevent data loss.")
22+
23+
ananta_hosts = convert_to_ananta_hosts(ssh_path)
24+
with open(csvfile, "w", encoding="utf-8") as file:
25+
file.writelines([host.to_string() for host in ananta_hosts])

0 commit comments

Comments
 (0)