osgos_postinstall/postinstall/__init__.py

395 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import subprocess
import sys
import signal
from dialog import Dialog
from postinstall.root import check_root
from postinstall.option import (
network,
update,
add_user,
install_packages,
ssh,
hostname,
shell,
database,
docker,
)
import json
check_root()
from postinstall.logger import logger
# import debugpy
# debugpy.listen(("0.0.0.0", 5678))
# debugpy.wait_for_client()
SSH_CONFIG_PATH = "/etc/ssh/sshd_config"
HOSTNAME_FILE = "/etc/hostname"
AUTOSTART_FILE = "/usr/share/postinstall/.norestart"
BACKUPS_FILE = "/usr/share/postinstall/backups.json"
UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server"
logging = logger
def get_hostname() -> str:
try:
with open(HOSTNAME_FILE, "r") as f:
return f.read().strip()
except IOError as e:
logging.error(f"Ошибка при чтении hostname: {e}")
return "localhost"
def get_ssh_port() -> str:
try:
result = subprocess.run(
["grep", "^#*Port", SSH_CONFIG_PATH],
capture_output=True,
text=True,
check=True,
)
port = result.stdout.strip().split()[-1]
return port if port.isdigit() else "22"
except subprocess.CalledProcessError:
logging.warning("Не удалось получить порт SSH, используется порт по умолчанию")
return "22"
class MenuScreen:
def __init__(self):
self.d = Dialog(dialog="dialog")
self.ssh_port = get_ssh_port()
self.hostname = get_hostname()
self.ssh_changes = None
self.new_hostname = None
self.network = None
self.autostart_enabled = self.check_auto_start()
self.new_autostart_enabled = self.autostart_enabled
self.options = [
{
"name": "Сетевые настройки",
"value": "",
"tabs": "\t \t",
"option": "network",
},
{
"name": "Обновление системы",
"value": "",
"tabs": "\t \t",
"option": "update",
},
{
"name": "Добавить пользователя",
"value": "",
"tabs": "\t \t",
"option": "add_user",
},
{
"name": "Установка программ",
"value": "",
"tabs": "\t \t",
"option": "install_packages",
},
{
"name": "Настройки ssh",
"value": "",
"tabs": "\t \t",
"option": "ssh",
},
{
"name": "Изменить доменное имя ПК",
"value": self.hostname,
"tabs": "\t \t",
"option": "hostname",
},
{"name": "Выбор оболочки", "value": "", "tabs": "\t \t", "option": "shell"},
{
"name": "Автозапуск при следующем запуске",
"value": "Включен" if self.new_autostart_enabled else "Отключен",
"tabs": "\t",
"option": "auto_start",
},
{
"name": "Настройка базы данных",
"value": "",
"tabs": "\t \t",
"option": "database",
},
{
"name": "Настройка docker",
"value": "",
"tabs": "\t \t",
"option": "docker",
},
{
"name": "Откат последних изменений",
"value": "",
"tabs": "\t \t",
"option": "rollback",
},
{
"name": "Сохранить и выйти",
"value": "",
"tabs": "\t \t",
"option": "save_and_exit",
},
]
os.makedirs(os.path.dirname(AUTOSTART_FILE), exist_ok=True)
def show_menu(self):
choices = []
for i, option in enumerate(self.options, 1):
value = option["value"]
if option["option"] == "hostname":
value = (
self.new_hostname
if self.new_hostname is not None
else self.hostname
)
if self.new_hostname is not None:
value = f"{value} (не применено)"
elif option["option"] == "network":
value = "Изменено (не применено)" if self.network else ""
choice_text = f"{option['name']}{option['tabs']}{value}"
choices.append((str(i), choice_text))
code, tag = self.d.menu(
"Выберите опцию:",
choices=choices,
title="Главное меню",
cancel_label="Выйти без сохранения",
width=70,
height=15,
)
if code == self.d.OK and tag:
return self.options[int(tag) - 1]["option"]
return None
def set_temp_network(self, settings):
self.temp_network = settings
def check_auto_start(self) -> bool:
return not os.path.exists(AUTOSTART_FILE)
def set_auto_start(self, enable: bool) -> None:
try:
if enable:
if os.path.exists(AUTOSTART_FILE):
os.remove(AUTOSTART_FILE)
logging.info("Автозапуск включен")
else:
with open(AUTOSTART_FILE, "w") as f:
f.write("# Это файл создан для отключения автозапуска\n")
logging.info("Автозапуск отключен")
except Exception as e:
logging.error(f"Ошибка при изменении автозапуска: {e}")
def update_ssh_port(self, new_port):
self.new_ssh_port = new_port
def update_hostname(self, new_hostname):
self.new_hostname = new_hostname
def update_autostart(self, new_state):
self.new_autostart_enabled = new_state
def choose(option, menu):
if menu.new_autostart_enabled != menu.autostart_enabled:
menu.set_auto_start(menu.new_autostart_enabled)
if option == "network":
net_config = network.NetworkConfig()
if net_config.run():
menu.network = net_config
elif option == "update":
logging.info("Начало обновления системы.")
update.run_update()
logging.info("Завершено обновление системы.")
elif option == "add_user":
logging.info("Началось добавления пользователя.")
add_user.run_add_user()
logging.info("Завершено добавление пользователя.")
elif option == "install_packages":
logging.info("Начало установки пакетов.")
install_packages.run_install_packages()
logging.info("Завершена установка пакетов.")
elif option == "ssh":
ssh_config, changes_made = ssh.run_ssh_config()
if changes_made:
menu.ssh_changes = ssh_config
elif option == "hostname":
new_hostname = hostname.run_change_hostname()
if new_hostname:
menu.new_hostname = new_hostname
menu.options[5]["value"] = f"{new_hostname} (не применено)"
elif option == "shell":
logging.info("Началось изменение shell.")
shell.run_change_shell()
logging.info("Завершено изменение shell.")
elif option == "auto_start":
menu.new_autostart_enabled = not menu.new_autostart_enabled
menu.options[7]["value"] = (
"Включен" if menu.new_autostart_enabled else "Отключен"
)
logging.info(
f"Автозапуск {'включен' if menu.new_autostart_enabled else 'отключен'}"
)
elif option == "database":
logging.info("Запуск настройки базы данных.")
# Открытие диалогового окна для выбора базы данных
code, choice = menu.d.menu("Выберите базу данных для установки:",
choices=[("PostgreSQL", "Установить PostgreSQL"),
("MariaDB", "Установить MariaDB")])
if code == menu.d.OK:
if choice == "PostgreSQL":
if not database.is_installed('postgresql-16'):
database.install_postgresql(menu.d)
else:
menu.d.msgbox("PostgreSQL и Harlequin уже установлены.")
elif choice == "MariaDB":
if not database.is_installed('mariadb-server'):
database.install_mariadb(menu.d)
else:
menu.d.msgbox("MariaDB уже установлена.")
# Независимо от выбранной базы данных, предложить установку Webmin
if not database.is_installed('webmin'):
code = menu.d.yesno("Установить Webmin?", yes_label="Да", no_label="Нет")
if code == menu.d.OK:
database.install_webmin(menu.d)
else:
menu.d.msgbox("Webmin уже установлен.")
logging.info("Настройка базы данных завершена.")
else:
menu.d.msgbox("Выбор базы данных отменён")
elif option == "docker":
docker.run()
elif option == "rollback":
try:
with open(BACKUPS_FILE, "r") as f:
backups = json.load(f)
f.close()
for i in backups:
with open(i["file"], "w") as f:
f.write(i["content"])
f.close()
subprocess.run(["systemctl", "restart", "networking"], check=True)
subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True)
ssh_port = get_ssh_port()
subprocess.run(
[
"sudo",
"sed",
"-i",
f"s/^ports=.*/ports={ssh_port}\/tcp/",
UFW_OPENSSH_CONFIG_PATH,
],
check=True,
)
subprocess.run(["sudo", "ufw", "app", "update", "OpenSSH"], check=True)
subprocess.run(["sudo", "ufw", "delete", "allow", "OpenSSH"], check=True)
subprocess.run(["sudo", "ufw", "allow", "OpenSSH"], check=True)
menu.d.msgbox("Резервные копии успешно восстановлены")
logging.info("Резервные копии успешно восстановлены")
except Exception as e:
menu.d.msgbox(f"Вы еще не применяли изменения.{e}")
return False
elif option == "save_and_exit":
with open("/etc/network/interfaces", "r") as f:
interfaces = f.read()
f.close()
with open("/etc/resolv.conf", "r") as f:
dns_settings = f.read()
f.close()
with open("/etc/ssh/sshd_config", "r") as f:
ssh_settings = f.read()
f.close()
try:
with open("/etc/ssh/sshd_config.d/auth.conf", "r") as f:
ssh_auth = f.read()
except Exception:
ssh_auth = ""
backup = [
{
"file": "/etc/network/interfaces",
"content": interfaces,
},
{
"file": "/etc/resolv.conf",
"content": dns_settings,
},
{
"file": "/etc/ssh/sshd_config",
"content": ssh_settings,
},
{
"file": "/etc/ssh/sshd_config.d/auth.conf",
"content": ssh_auth,
},
]
with open(BACKUPS_FILE, "w") as f:
f.write(json.dumps(backup))
f.close()
if menu.ssh_changes:
logging.info(
f"Применены новые настройки SSH. {'Новый порт SSH: ' + str(menu.ssh_changes.new_port) if menu.ssh_changes.new_port is not None else ''} {'Новый метод авторизации: ' + menu.ssh_changes.new_auth_method if menu.ssh_changes.new_auth_method is not None else ''}"
)
menu.ssh_changes.apply_changes()
if menu.new_hostname:
logging.info(f"Применено новое имя хоста: {menu.new_hostname}")
if hostname.apply_hostname_change(menu.new_hostname):
menu.hostname = menu.new_hostname
if menu.network:
logging.info(
f"Применены новые настройки сети: {menu.network.temp_network_settings}"
)
if not menu.network.save_and_apply_network_settings():
return True
return False
return True
def signal_handler(sig, frame):
logging.info("Приложение закрыто")
subprocess.run("clear", shell=True)
sys.exit(0)
def main():
logging.info("Приложение запущено")
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
menu = MenuScreen()
while True:
option = menu.show_menu()
if option is None:
break
if not choose(option, menu):
break
logging.info("Приложение закрыто")
subprocess.run("clear", shell=True)
if __name__ == "__main__":
main()