231 lines
6.4 KiB
Python
231 lines
6.4 KiB
Python
|
|
# NOTE:
|
|
|
|
|
|
import base64
|
|
import io
|
|
import shlex
|
|
from datetime import datetime
|
|
|
|
import paramiko
|
|
import yaml
|
|
|
|
# Servers to update
|
|
SERVERS = [
|
|
"172.16.10.123",
|
|
"172.16.10.110",
|
|
"172.16.10.107",
|
|
]
|
|
|
|
# SSH configuration
|
|
USERNAME = "cezen"
|
|
PASSWORD = "17lamborghini"
|
|
REMOTE_YAML = "/home/cezen/Desktop/mumble-server/docker-compose.yml"
|
|
BACKUP_YAML = REMOTE_YAML + ".bak"
|
|
VERSION_DIR = "/home/cezen/Desktop/mumble-server/config_versions"
|
|
VERSION_KEEP_LIMIT = 10
|
|
VERSION_FILE_PREFIX = "docker-compose"
|
|
|
|
# Service whose environment section will be edited
|
|
SERVICE_NAME = "mumble"
|
|
|
|
|
|
def run_sudo_command(ssh, command):
|
|
full_command = f"echo '{PASSWORD}' | sudo -S {command}"
|
|
stdin, stdout, stderr = ssh.exec_command(full_command)
|
|
exit_code = stdout.channel.recv_exit_status()
|
|
|
|
if exit_code != 0:
|
|
raise RuntimeError(stderr.read().decode("utf-8"))
|
|
|
|
return stdout.read().decode("utf-8") if stdout.readable() else ""
|
|
|
|
|
|
def read_remote_file(ssh, path):
|
|
return run_sudo_command(ssh, f'cat "{path}"')
|
|
|
|
|
|
def write_remote_file(ssh, path, content):
|
|
# Encode content to base64 to avoid all shell quoting/escaping issues
|
|
encoded = base64.b64encode(content.encode("utf-8")).decode("ascii")
|
|
run_sudo_command(ssh, f"bash -c 'echo {encoded} | base64 -d > \"{path}\"'")
|
|
|
|
|
|
def ensure_version_dir(ssh):
|
|
run_sudo_command(ssh, f"mkdir -p {shlex.quote(VERSION_DIR)}")
|
|
|
|
|
|
def write_config_version(ssh, timestamp, content):
|
|
ensure_version_dir(ssh)
|
|
version_path = f"{VERSION_DIR}/{VERSION_FILE_PREFIX}_{timestamp}.yml"
|
|
write_remote_file(ssh, version_path, content)
|
|
prune_config_versions(ssh)
|
|
return version_path
|
|
|
|
|
|
def prune_config_versions(ssh):
|
|
keep_from = VERSION_KEEP_LIMIT + 1
|
|
cleanup_script = (
|
|
f"cd {shlex.quote(VERSION_DIR)} 2>/dev/null || exit 0\n"
|
|
f"ls -1t {VERSION_FILE_PREFIX}_*.yml 2>/dev/null "
|
|
f"| tail -n +{keep_from} "
|
|
"| xargs -r rm --"
|
|
)
|
|
run_sudo_command(ssh, f"bash -c {shlex.quote(cleanup_script)}")
|
|
|
|
|
|
def backup_remote_file(ssh):
|
|
run_sudo_command(ssh, f'cp "{REMOTE_YAML}" "{BACKUP_YAML}"')
|
|
|
|
|
|
def parse_environment(env_list):
|
|
"""
|
|
Converts:
|
|
["KEY1=value1", "KEY2=value2"]
|
|
To:
|
|
[("KEY1", "value1"), ("KEY2", "value2")]
|
|
"""
|
|
result = []
|
|
for item in env_list:
|
|
if isinstance(item, str) and "=" in item:
|
|
key, value = item.split("=", 1)
|
|
result.append((key, value))
|
|
return result
|
|
|
|
|
|
def format_environment(pairs):
|
|
"""
|
|
Converts:
|
|
[("KEY1", "value1"), ("KEY2", "value2")]
|
|
To:
|
|
["KEY1=value1", "KEY2=value2"]
|
|
"""
|
|
return [f"{key}={value}" for key, value in pairs]
|
|
|
|
|
|
def load_environment_from_server(host, password):
|
|
print(f"Connecting to {host}...")
|
|
|
|
ssh = paramiko.SSHClient()
|
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
|
|
try:
|
|
ssh.connect(hostname=host, username=USERNAME, password=password, timeout=10)
|
|
|
|
content = read_remote_file(ssh, REMOTE_YAML)
|
|
data = yaml.safe_load(content)
|
|
|
|
env_list = data["services"][SERVICE_NAME].get("environment", [])
|
|
env_pairs = parse_environment(env_list)
|
|
|
|
return ssh, data, env_pairs, content
|
|
|
|
except Exception:
|
|
ssh.close()
|
|
raise
|
|
|
|
|
|
def save_environment_to_server(ssh, data, env_pairs):
|
|
data["services"][SERVICE_NAME]["environment"] = format_environment(env_pairs)
|
|
|
|
# Create backup first
|
|
backup_remote_file(ssh)
|
|
|
|
# Serialize YAML
|
|
output = io.StringIO()
|
|
yaml.dump(data, output, default_flow_style=False, sort_keys=False)
|
|
|
|
# Write updated file
|
|
write_remote_file(ssh, REMOTE_YAML, output.getvalue())
|
|
|
|
|
|
def save_versions_after_success(previous_configs, timestamp):
|
|
version_results = {}
|
|
|
|
for host, content in previous_configs.items():
|
|
try:
|
|
ssh = paramiko.SSHClient()
|
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
ssh.connect(hostname=host, username=USERNAME, password=PASSWORD, timeout=10)
|
|
try:
|
|
version_results[host] = write_config_version(ssh, timestamp, content)
|
|
finally:
|
|
ssh.close()
|
|
except Exception as e:
|
|
version_results[host] = str(e)
|
|
|
|
return version_results
|
|
|
|
|
|
def edit_environment(env_pairs, json_data):
|
|
print(env_pairs,"ewejnwejnwejwej")
|
|
updated = []
|
|
for key, value in env_pairs:
|
|
new_value = json_data.get(key, "")
|
|
if new_value == "":
|
|
updated.append((key, value)) # keep original
|
|
else:
|
|
updated.append((key, new_value))
|
|
return updated
|
|
|
|
|
|
def run(json_data, servers=None):
|
|
target_servers = list(servers or SERVERS)
|
|
if not target_servers:
|
|
return {
|
|
"status": "error",
|
|
"message": "Select at least one VM to update.",
|
|
"results": {},
|
|
}
|
|
|
|
first_host = target_servers[0]
|
|
|
|
try:
|
|
ssh, data, env_pairs, _ = load_environment_from_server(first_host, PASSWORD)
|
|
ssh.close()
|
|
except Exception as e:
|
|
return {"status": "error", "message": f"Failed to connect to {first_host}: {str(e)}"}
|
|
|
|
updated_env = edit_environment(env_pairs, json_data)
|
|
|
|
results = {}
|
|
previous_configs = {}
|
|
for host in target_servers:
|
|
try:
|
|
ssh, server_data, _, previous_content = load_environment_from_server(host, PASSWORD)
|
|
try:
|
|
save_environment_to_server(ssh, server_data, updated_env)
|
|
results[host] = "success"
|
|
previous_configs[host] = previous_content
|
|
finally:
|
|
ssh.close()
|
|
except Exception as e:
|
|
results[host] = str(e)
|
|
|
|
if any(result != "success" for result in results.values()):
|
|
return {
|
|
"status": "error",
|
|
"message": "Config was not saved successfully to all selected VMs. No version files were created.",
|
|
"results": results,
|
|
}
|
|
|
|
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
version_results = save_versions_after_success(previous_configs, timestamp)
|
|
|
|
if any(
|
|
not str(result).startswith(f"{VERSION_DIR}/")
|
|
for result in version_results.values()
|
|
):
|
|
return {
|
|
"status": "error",
|
|
"message": "Config was saved to all selected VMs, but one or more version files could not be created.",
|
|
"results": results,
|
|
"version_results": version_results,
|
|
}
|
|
|
|
return {
|
|
"status": "success",
|
|
"results": results,
|
|
"version_results": version_results,
|
|
}
|