186 lines
6.4 KiB
Python
186 lines
6.4 KiB
Python
import os
|
|
import subprocess
|
|
import asyncio
|
|
from .utils import (
|
|
print_info,
|
|
print_warning,
|
|
print_error,
|
|
safe_input
|
|
)
|
|
from .list_hosts import (
|
|
build_host_list_table,
|
|
load_config_file,
|
|
gather_host_info,
|
|
sort_by_ip
|
|
)
|
|
|
|
async def regenerate_key(conf_dir):
|
|
"""
|
|
Regenerate the SSH key for a chosen host by:
|
|
1) Displaying the unified table of hosts (No. | Host | User | Port | HostName | IP Address | Conf Directory).
|
|
2) Letting you pick a row number or host label.
|
|
3) Reading/deleting any existing local keys,
|
|
4) Generating a new key,
|
|
5) Optionally copying it to the remote,
|
|
6) Removing the old pub key from the remote authorized_keys if present.
|
|
"""
|
|
|
|
print_info("Regenerate Key - Step 1: Show current hosts...\n")
|
|
|
|
# 1) Reuse the same columns as the main 'list_hosts'
|
|
headers, final_data = await build_host_list_table(conf_dir)
|
|
if not final_data:
|
|
print_warning("No hosts found. Cannot regenerate a key.")
|
|
return
|
|
|
|
from tabulate import tabulate
|
|
print("\nSSH Conf Subdirectory Host List (Sorted by IP Ascending)")
|
|
print(tabulate(final_data, headers=headers, tablefmt="grid"))
|
|
|
|
# 2) We need to correlate row => actual config block.
|
|
# We'll replicate the logic that build_host_list_table uses.
|
|
all_blocks = []
|
|
import glob
|
|
for cfile in glob.glob(os.path.join(conf_dir, "*", "config")):
|
|
all_blocks.extend(load_config_file(cfile))
|
|
|
|
results = await gather_host_info(all_blocks)
|
|
sorted_rows = sort_by_ip(results)
|
|
# sorted_rows is a list of 7-tuples:
|
|
# (host_label, user, colored_port, hostname, colored_ip, conf_path, raw_ip)
|
|
|
|
choice = safe_input("Enter the row number or the Host label to regenerate: ")
|
|
if choice is None:
|
|
return
|
|
choice = choice.strip()
|
|
if not choice:
|
|
print_error("No choice given.")
|
|
return
|
|
|
|
target_tuple = None
|
|
if choice.isdigit():
|
|
row_idx = int(choice)
|
|
if row_idx < 1 or row_idx > len(sorted_rows):
|
|
print_warning(f"Invalid row number {row_idx}.")
|
|
return
|
|
target_tuple = sorted_rows[row_idx - 1]
|
|
else:
|
|
# user typed a label
|
|
for t in sorted_rows:
|
|
if t[0] == choice: # t[0] is host_label
|
|
target_tuple = t
|
|
break
|
|
if not target_tuple:
|
|
print_warning(f"No matching host label '{choice}' found.")
|
|
return
|
|
|
|
# 3) Retrieve the full config block so we have Port, IdentityFile, etc.
|
|
host_label = target_tuple[0]
|
|
hostname = target_tuple[3] # (host_label, user, colored_port, hostname, ...)
|
|
user = target_tuple[1]
|
|
|
|
# find that block
|
|
found_block = None
|
|
for b in all_blocks:
|
|
if b.get("Host") == host_label:
|
|
found_block = b
|
|
break
|
|
|
|
if not found_block:
|
|
print_warning(f"No config block found for '{host_label}'.")
|
|
return
|
|
|
|
port_str = found_block.get("Port", "22")
|
|
port = int(port_str)
|
|
identity_file = found_block.get("IdentityFile", "")
|
|
|
|
# If missing or "N/A", we can't regenerate
|
|
if not identity_file or identity_file == "N/A":
|
|
print_error("No IdentityFile found in config; cannot regenerate.")
|
|
return
|
|
|
|
# 4) Remove old local key files
|
|
expanded_key = os.path.expanduser(identity_file)
|
|
pub_path = expanded_key + ".pub"
|
|
old_pub_data = ""
|
|
|
|
if os.path.isfile(pub_path):
|
|
try:
|
|
with open(pub_path, "r") as f:
|
|
old_pub_data = f.read().rstrip("\n")
|
|
except Exception as e:
|
|
print_warning(f"Could not read old pub key: {e}")
|
|
|
|
print_info("Removing old key files locally...")
|
|
for path in [expanded_key, pub_path]:
|
|
if os.path.isfile(path):
|
|
try:
|
|
os.remove(path)
|
|
print_info(f"Removed {path}")
|
|
except Exception as e:
|
|
print_warning(f"Could not remove {path}: {e}")
|
|
|
|
# 5) Generate new key
|
|
print_info("Generating new ed25519 SSH key...")
|
|
new_key_path = expanded_key
|
|
cmd = ["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", new_key_path]
|
|
try:
|
|
subprocess.check_call(cmd)
|
|
print_info(f"Generated new SSH key at {new_key_path}")
|
|
except subprocess.CalledProcessError as e:
|
|
print_error(f"Error generating new SSH key: {e}")
|
|
return
|
|
|
|
# 6) Copy the new key to remote if user wants
|
|
copy_choice = safe_input("Copy new key to remote now? (y/n): ")
|
|
if copy_choice and copy_choice.lower().startswith('y'):
|
|
ssh_copy_cmd = ["ssh-copy-id", "-i", new_key_path]
|
|
if port != 22:
|
|
ssh_copy_cmd += ["-p", str(port)]
|
|
ssh_copy_cmd.append(f"{user}@{hostname}")
|
|
try:
|
|
subprocess.check_call(ssh_copy_cmd)
|
|
print_info("New key successfully copied to remote server.")
|
|
except subprocess.CalledProcessError as e:
|
|
print_error(f"Error copying new key: {e}")
|
|
|
|
# 7) Remove old key from authorized_keys if we had old_pub_data
|
|
if old_pub_data:
|
|
print_info("Attempting to remove old key from remote authorized_keys...")
|
|
await remove_old_key_remote(old_pub_data, user, hostname, port)
|
|
else:
|
|
print_warning("No old pub key data found locally, skipping remote removal.")
|
|
|
|
async def remove_old_key_remote(old_pub_data, user, hostname, port):
|
|
"""
|
|
Checks and removes the exact matching line from remote authorized_keys
|
|
via grep -Fxq / grep -vFx.
|
|
"""
|
|
check_cmd = [
|
|
"ssh", "-o", "StrictHostKeyChecking=no",
|
|
"-p", str(port),
|
|
f"{user}@{hostname}",
|
|
f'grep -Fxq "{old_pub_data}" ~/.ssh/authorized_keys'
|
|
]
|
|
found_key = False
|
|
try:
|
|
subprocess.check_call(check_cmd)
|
|
found_key = True
|
|
except subprocess.CalledProcessError:
|
|
pass
|
|
|
|
if not found_key:
|
|
print_warning("No old key found on remote authorized_keys.")
|
|
return
|
|
|
|
remove_cmd = [
|
|
"ssh", "-o", "StrictHostKeyChecking=no",
|
|
"-p", str(port),
|
|
f"{user}@{hostname}",
|
|
f'grep -vFx "{old_pub_data}" ~/.ssh/authorized_keys > ~/.ssh/tmp && mv ~/.ssh/tmp ~/.ssh/authorized_keys'
|
|
]
|
|
try:
|
|
subprocess.check_call(remove_cmd)
|
|
print_info("Old public key removed from remote authorized_keys.")
|
|
except subprocess.CalledProcessError:
|
|
print_warning("Failed to remove old key from remote authorized_keys (permissions or other error).")
|