diff --git a/docker-compose.yml b/docker-compose.yml index 55189d536165ab8b14e4ae9da3ab9314009e7c6d..6e44265732f1bb7b7415ae6659a657abbe21025e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,8 @@ services: APP_MODE: 'dev' TELEGRAM_BOT_TOKEN: 'your dev token here' volumes: - - ~/.ssh:/root/.ssh + - ~/.ssh/id_rsa:/root/.ssh/id_rsa + - ~/.ssh/id_rsa.pub:/root/.ssh/id_rsa.pub - ./:/app working_dir: '/app' networks: diff --git a/src/actions/__init__.py b/src/actions/__init__.py index f689a298030bdb793cabc394d600864289f1ba2e..23a65a80c4baa198215f907419bd3838e80d10ad 100644 --- a/src/actions/__init__.py +++ b/src/actions/__init__.py @@ -4,6 +4,7 @@ from .interactive_session import * from .permissions import * from .server_management import * from .stats import * +from .systemctl import * from ..models.crud.server_crud import fn_get_all_chat_ids diff --git a/src/actions/basic_actions.py b/src/actions/basic_actions.py index 5c35909c6f12b7373d32e0bfe0bc744a5b012d6c..9b48d46518407851ad96c16c6fa2772e51aa68a6 100644 --- a/src/actions/basic_actions.py +++ b/src/actions/basic_actions.py @@ -21,6 +21,7 @@ async def overall_help(message: Message, state=None): # '* meme admin commands - Manage stored commands\n\n' '* meme admin exec {alias} {command} - Execute single command\n\n' '* meme admin docker - Manage docker on remote server\n\n' + '* meme admin sys - Manage systemctl on remote server\n\n' '* meme admin stats - See server statistics\n\n' '```', parse_mode=ParseMode.MARKDOWN diff --git a/src/actions/systemctl.py b/src/actions/systemctl.py new file mode 100644 index 0000000000000000000000000000000000000000..8a922405136dfe1dedc2b84aa89c7bb16c4c9518 --- /dev/null +++ b/src/actions/systemctl.py @@ -0,0 +1,112 @@ +import logging +import re + +from aiogram.types import Message, ParseMode + +from src.models.server import ServerPermissions +from src.utils.debug_mode import debug_message +from src.utils.decorators import ( + bot_action +) +from src.utils.server_utils import get_server_by_alias +from src.utils.ssh import run_ssh_command + +logger = logging.getLogger(__name__) + + +@bot_action(r'meme admin sys( help)?$') +async def overall_help(message: Message, state=None): + await message.reply( + '```\n' + '* sys stats {alias} {service} - Check status of the service\n\n' + '* sys start {alias} {service} - Start the service\n\n' + '* sys stop {alias} {service} - Stop the service\n\n' + '* sys restart {alias} {service} - Restart the service\n\n' + '* sys cat {alias} {service} - Show one or more service unit files\n\n' + '```', + parse_mode=ParseMode.MARKDOWN + ) + + +@bot_action(r'meme admin sys stats', + params=[ + ('alias', r'[\w\d]+'), + ('service', r'[\w\d]+\.service') + ]) +async def status(message: Message, alias, service, state): + server = await get_server_by_alias(message, + server_alias=alias, + minimal_permission=ServerPermissions.SERVICES) + service_info = await run_ssh_command(server, f'systemctl status {service}') + description = re.search(r'(.*) - (?P<description>.*)', service_info.splitlines()[0]) + startup_file = re.search(r'(.*)Loaded: loaded \((?P<file>.*\.service)', service_info) + service_state = re.search(r'(.*)Active: (?P<state>.*) since (.*); (?P<uptime>.*)', service_info) + pid = re.search(r'(.*)Main PID: (?P<pid>.*)', service_info) + tasks = re.search(r'(.*)Tasks: (?P<tasks>.*)', service_info) + memory = re.search(r'(.*)Memory: (?P<memory>.*)', service_info) + res_message = "```\n" + await debug_message(message, service_info) + for regex in [description, startup_file, service_state, pid, tasks, memory]: + if regex is None: + continue + for key, val in regex.groupdict().items(): + res_message += f'{key}: {val}\n\n' + res_message += '```' + await message.reply(res_message, parse_mode=ParseMode.MARKDOWN) + + +@bot_action(r'meme admin sys start', + params=[ + ('alias', r'[\w\d]+'), + ('service', r'[\w\d]+\.service') + ]) +async def start(message: Message, alias, service, state): + server = await get_server_by_alias(message, + server_alias=alias, + minimal_permission=ServerPermissions.SERVICES) + await run_ssh_command(server, f'systemctl start {service}') + await message.reply("Service started") + + +@bot_action(r'meme admin sys stop', + params=[ + ('alias', r'[\w\d]+'), + ('service', r'[\w\d]+\.service') + ]) +async def stop(message: Message, alias, service, state): + server = await get_server_by_alias(message, + server_alias=alias, + minimal_permission=ServerPermissions.SERVICES) + await run_ssh_command(server, f'systemctl stop {service}') + await message.reply("Service stopped") + + +@bot_action(r'meme admin sys restart', + params=[ + ('alias', r'[\w\d]+'), + ('service', r'[\w\d]+\.service') + ]) +async def restart(message: Message, alias, service, state): + server = await get_server_by_alias(message, + server_alias=alias, + minimal_permission=ServerPermissions.SERVICES) + await run_ssh_command(server, f'systemctl restart {service}') + await message.reply("Service restarted") + + +@bot_action(r'meme admin sys cat', + params=[ + ('alias', r'[\w\d]+'), + ('service', r'[\w\d]+\.service') + ]) +async def cat(message: Message, alias, service, state): + server = await get_server_by_alias(message, + server_alias=alias, + minimal_permission=ServerPermissions.SERVICES) + out = await run_ssh_command(server, f'systemctl cat {service}') + await message.reply( + '```\n' + f'{out}' + '```', + parse_mode=ParseMode.MARKDOWN + ) diff --git a/src/utils/ssh.py b/src/utils/ssh.py index ff8a2c9107fc0c813b5bfeb43bc0ac63bb4ac42c..7452cbc280354754f53c4104a3fe827a05e989fd 100644 --- a/src/utils/ssh.py +++ b/src/utils/ssh.py @@ -10,26 +10,35 @@ logger = logging.getLogger() async def open_ssh_session(server: Server) -> asyncssh.SSHClientProcess: - connection = await asyncssh.connect(server.server_address, server.server_port) + connection = await asyncssh.connect(server.server_address, server.server_port, known_hosts=None) process = await connection.create_process('/bin/bash') return process -async def run_interactive_command(command: str, - process: asyncssh.SSHClientProcess, - timeout=0.5) -> str: - process.stdin.write(command + '\n') +async def collect_output(out_pipe, timeout): res = [] try: - line = await asyncio.wait_for(process.stdout.readline(), timeout) + line = await asyncio.wait_for(out_pipe.readline(), timeout) res.append(line) while line: - logger.debug(line) - res.append(await asyncio.wait_for(process.stdout.readline(), timeout)) - except asyncio.exceptions.TimeoutError as e: - logger.exception(e) - return '\n'.join(res).strip() - return '\n'.join(res).strip() + logger.info(line) + res.append(await asyncio.wait_for(out_pipe.readline(), timeout)) + except asyncio.exceptions.TimeoutError: + return ''.join(res).strip() + return ''.join(res).strip() + + +async def run_interactive_command(command: str, + process: asyncssh.SSHClientProcess, + timeout=0.5) -> str: + process.stdin.write(command + '\n') + stdout = await collect_output(process.stdout, timeout) + stderr = await collect_output(process.stderr, timeout) + logger.debug(f"STDOUT: {stdout}") + logger.debug(f"STDERR: {stderr}") + if stderr: + raise Exception(stderr) + return stdout async def run_ssh_command(server: Server, command: str) -> str: