diff --git a/deploy.sh b/deploy.sh index 21c771bc7e64c4578653f68b40f583833c15518f..5b32174a5740fde435f71f82d0455cfc6b6cb6eb 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,5 +1,11 @@ #!/bin/bash echo "TELEGRAM_BOT_TOKEN=${BOT_TOKEN}" >>.env.docker + +# Store some info in app.info file +printf "Maintainer: \`win10@list.ru\`\n" >app.info +printf "repo url: \`%s\`\n" "$CI_PROJECT_URL" >>app.info +printf "last commit message: \`\`\`\n%s \`\`\`\n" "$(git log -1 --pretty=%B)" >>app.info + rm -rf "${APP_DIR:?}/"* rsync -av --exclude=".git" . "${APP_DIR}" cd "${APP_DIR}" || exit 1 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 71aa015cf001149280d751b3ace80dda5e562838..aec3e5057c0c12f6d4f24a5d0836b9896c72e485 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -17,6 +17,7 @@ services: POSTGRES_USER: 'telegram_sysadmin' POSTGRES_HOST: 'system_bot_postgres' POSTGRES_PORT: 5432 + APP_MODE: 'prod' volumes: - ~/.ssh:/root/.ssh - ./:/app diff --git a/docker-compose.yml b/docker-compose.yml index 2b3e15ce8cb13932af9d68f81a0d6f2d6cca4136..55189d536165ab8b14e4ae9da3ab9314009e7c6d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,7 @@ services: POSTGRES_USER: 'dev_telegram_sysadmin' POSTGRES_HOST: 'system_bot_postgres' POSTGRES_PORT: 5432 + APP_MODE: 'dev' TELEGRAM_BOT_TOKEN: 'your dev token here' volumes: - ~/.ssh:/root/.ssh diff --git a/src/__init__.py b/src/__init__.py index 446d9fd1b2deff9fe24e817676ce484913b2c503..7b08a4fc09423a58799422b90869658c6cb7b985 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,2 +1,2 @@ -__version__ = '0.0.1' +__version__ = '0.0.2' diff --git a/src/actions/basic_actions.py b/src/actions/basic_actions.py index 5e11b69adc7c0db5a749b43ac93e50eb66a264ed..fa6be86e8d8e213d2b549e29d1afc65d975e4edf 100644 --- a/src/actions/basic_actions.py +++ b/src/actions/basic_actions.py @@ -1,6 +1,8 @@ from aiogram.types import Message, ParseMode +from src import __version__ as bot_version from src.models.server import ServerPermissions +from src.settings import settings from src.utils import chunks from src.utils.decorators import ( bot_action @@ -39,3 +41,31 @@ async def bot_run_code(message: Message, alias: str, command: str, state=None): results = chunks(await run_ssh_command(server, command), 4095) for res in results: await message.reply(f'```\n{res}```', parse_mode=ParseMode.MARKDOWN) + + +@bot_action(trigger_str='meme admin toggle debug') +async def debug_mode_toggle(message: Message, state=None): + if settings.app_mode != 'dev': + await message.reply('Debug mode is unavailable in production.') + return + if settings.debug_mode: + settings.debug_mode = False + await message.reply('Leaving debug mode') + else: + settings.debug_mode = True + await message.reply('Entering debug mode') + + +@bot_action(trigger_str='meme admin info') +async def bot_info(message: Message, state): + if settings.app_mode == 'prod': + with open('app.info') as f: + app_info = f.read() + else: + app_info = f'App mode: `{settings.app_mode}`' + await message.reply( + '* Le-memese system administrator info *\n\n' + f'version: `{bot_version}`\n' + f'{app_info}' + , parse_mode=ParseMode.MARKDOWN + ) diff --git a/src/actions/interactive_session.py b/src/actions/interactive_session.py index a213be1644f29f1943036bfe179fafb5cfcfccff..c731370449df2eebeaff8947952c2ccb459881e6 100644 --- a/src/actions/interactive_session.py +++ b/src/actions/interactive_session.py @@ -16,9 +16,10 @@ from aiogram.types import ( from src.models.crud.server_crud import fn_get_server from src.models.server import ServerPermissions from src.settings import settings +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 session_manager +from src.utils.ssh import session_manager, run_ssh_command logger = logging.getLogger() @@ -42,6 +43,12 @@ async def start_interactive_mode(message: Message, state=None): if not user_available_servers: raise Exception( 'Cannot start interactive session.\n\nNo server with remote command execution is available for you.') + await debug_message(message, + f'Chat id: {message.chat.id}\n' + f'Available rce servers: {list(map(lambda x: x.server_alias, user_available_servers))}\n' + f'Current user: {str(message.from_user.id)}\n' + ) + await UserStates.initial.set() markup = ReplyKeyboardMarkup(resize_keyboard=True, selective=True) for server in user_available_servers: @@ -51,15 +58,22 @@ async def start_interactive_mode(message: Message, state=None): @bot_action(filter_state=UserStates.initial) async def choose_server(message: Message, state: FSMContext): - async with state.proxy() as data: - logger.debug(data) server_alias = message.text chosen_server = await get_server_by_alias(message, server_alias) + await debug_message(message, + f'Chat id: {message.chat.id}\n' + f'Server permissions: {chosen_server.server_permissions.name} \n' + f'Current user: {str(message.from_user.id)}\n' + ) + if chosen_server.server_permissions.value < ServerPermissions.RCE.value \ and str(message.from_user.id) != chosen_server.server_admin: raise Exception('You do not have permission for remote code execution.' ' Please ask user who added this server to change permission level.') + # Dummy command to check that server is online + await run_ssh_command(chosen_server, 'uname') + async with state.proxy() as current_state: current_state['server_alias'] = chosen_server.server_alias current_state['session_id'] = await session_manager.add_connection(chosen_server) @@ -91,5 +105,10 @@ async def exit_session(message: Message, state: FSMContext): @bot_action(filter_state=UserStates.interactive) async def run_interactive_command(message: Message, state: FSMContext): async with state.proxy() as current_state: + await debug_message(message, + f'Chat id: {message.chat.id}\n' + f'Current user: {str(message.from_user.id)}\n' + f'Session id: {current_state["session_id"]}\n' + ) result = await session_manager.run_command(current_state['session_id'], message.text) await message.reply(f'```\n{result if result else "Nothing to show"}```', parse_mode=ParseMode.MARKDOWN) diff --git a/src/actions/permissions.py b/src/actions/permissions.py index 57cb1bbcd85b391318e8c24f0422b1628e39d2e8..5405adb9fcb0b21284446fbbf42858523a2d0e49 100644 --- a/src/actions/permissions.py +++ b/src/actions/permissions.py @@ -5,6 +5,7 @@ from aiogram.types import Message, ParseMode from src.models.crud.server_crud import fn_update_server from src.models.server import ServerPermissions from src.settings import settings +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 @@ -36,6 +37,14 @@ async def list_permissions(message: Message, state=None): ]) async def change_permissions(message: Message, server_alias, permission, state=None): server = await get_server_by_alias(message, server_alias) + await debug_message(message, + f'Chat id: {message.chat.id}\n' + f'Server alias: {server_alias}\n' + f'Requested permission: {permission}\n' + f'Server admin: {server.server_admin}\n' + f'Current user: {str(message.from_user.id)}\n' + ) + if not (str(message.from_user.id) == server.server_admin): raise Exception('You have no permissions to change server permissions.\nOnly admins can do this.') if permission.upper() not in ServerPermissions.__members__: diff --git a/src/actions/server_management.py b/src/actions/server_management.py index 6ba1e534fc7d7c70c684161eafab4b9994719877..aaf38eee91956c1ee6053787437256c9b785a730 100644 --- a/src/actions/server_management.py +++ b/src/actions/server_management.py @@ -10,6 +10,7 @@ from src.models.crud.server_crud import ( ) from src.models.server import ServerPermissions, Server from src.settings import settings +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 @@ -35,6 +36,14 @@ async def servers_help(message: Message, state=None): ('alias', r'[\w\d]+') ]) async def add_server(message: Message, address, alias, port, state=None): + await debug_message(message, + f'Chat id: {message.chat.id}\n' + f'Address: {address}\n' + f'Port: {port}\n' + f'Alias: {alias}\n' + f'Current user: {str(message.from_user.id)}\n' + ) + await fn_create_server(settings.engine, chat_id=str(message.chat.id), server_address=address, @@ -57,6 +66,13 @@ async def add_server(message: Message, address, alias, port, state=None): ]) async def rename_server(message: Message, old_alias, new_alias, state=None): server = await get_server_by_alias(message, old_alias) + await debug_message(message, + f'Chat id: {message.chat.id}\n' + f'Old alias: {old_alias}\n' + f'New alias: {new_alias}\n' + f'Server admin: {server.server_admin}\n' + f'Current user: {str(message.from_user.id)}\n' + ) if server.server_admin != str(message.from_user.id): raise Exception(f'You do not have enough permissions to rename this server. \nOnly server admins can do this.') await fn_update_server(settings.engine, @@ -72,7 +88,14 @@ async def rename_server(message: Message, old_alias, new_alias, state=None): ]) async def delete_server(message: Message, server_alias, state=None): server = await get_server_by_alias(message, server_alias) - if (server.server_admin != str(message.from_user.id) or + await debug_message(message, + f'Requested server: {server_alias}\n' + f'Server permission value: {server.server_permissions.value}' + f' ({server.server_permissions.name})\n' + f'Server admin: {server.server_admin}\n' + f'Current user: {str(message.from_user.id)}\n' + ) + if (server.server_admin != str(message.from_user.id) and server.server_permissions.value < ServerPermissions.DELETE.value): raise Exception(f'You do not have enough permissions to delete this server. \nOnly server admins can do this.') @@ -100,6 +123,10 @@ async def list_servers(message: Message, state=None): servers_list.append(await server_repr(server)) total_number = len(servers_list) servers_list = "\n".join(servers_list) + await debug_message(message, + f'Chat id: {message.chat.id}\n' + f'Total length: {total_number}\n' + f'Current user: {str(message.from_user.id)}\n') await message.reply( f'All available servers: \nTotal: {total_number} \n\n{servers_list if servers_list else "No servers available"}' , parse_mode=ParseMode.MARKDOWN) diff --git a/src/actions/stats.py b/src/actions/stats.py index 0b894864312f2e0799dba2d04b5ffdb3edf13e47..0f9226ccf62db218fd95b65f4543eb43002b0bb4 100644 --- a/src/actions/stats.py +++ b/src/actions/stats.py @@ -2,6 +2,7 @@ import logging from aiogram.types import Message, ParseMode +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 @@ -29,6 +30,12 @@ async def cpu_usage(message: Message, alias: str, state=None): server = await get_server_by_alias(message, alias) cpu_stats = await run_ssh_command(server, "grep 'cpu' /proc/stat") cores = cpu_stats.splitlines() + await debug_message(message, + f'Chat id: {message.chat.id}\n' + f'Current user: {str(message.from_user.id)}\n' + f'Server cpu info: \n' + f'{cpu_stats}' + ) def calculate_load(core_stats: str): logger.warning(core_stats) @@ -61,6 +68,12 @@ async def ram_usage(message: Message, alias: str, state=None): server = await get_server_by_alias(message, alias) ram_stats = await run_ssh_command(server, "free -m") table_head, *memories = ram_stats.splitlines() + await debug_message(message, + f'Chat id: {message.chat.id}\n' + f'Current user: {str(message.from_user.id)}\n' + f'Server RAM info: \n' + f'{ram_stats}' + ) def parse_stats(memory_stat: str): m_split = memory_stat.split() @@ -88,6 +101,13 @@ async def mem(message: Message, alias: str, state=None): ram_stats = await run_ssh_command(server, "df -hl /") table_head, info = ram_stats.splitlines() info = info.split() + await debug_message(message, + f'Chat id: {message.chat.id}\n' + f'Current user: {str(message.from_user.id)}\n' + f'Server disk info: \n' + f'{ram_stats}' + ) + await message.reply( '* Current memory usage: *\n\n' '```\n' diff --git a/src/settings.py b/src/settings.py index d5634def929ec0ffde0e85ebf3364be2396aec5b..aecab7b18e578f2f530d0d4935644d54ca555b8b 100644 --- a/src/settings.py +++ b/src/settings.py @@ -45,10 +45,16 @@ class Settings(BaseSettings): default=5432, env='POSTGRES_PORT') + app_mode: str = Field(name='app_mode', + default='dev', + env='APP_MODE') + engine: Engine = None dispatcher: Dispatcher = None + debug_mode: bool = False + @property def public_key(self) -> str: with open(expanduser('~/.ssh/id_rsa.pub'), 'r') as key: diff --git a/src/utils/debug_mode.py b/src/utils/debug_mode.py new file mode 100644 index 0000000000000000000000000000000000000000..6422a73bb01074d6f6b67f75a23f7bc01785d7d9 --- /dev/null +++ b/src/utils/debug_mode.py @@ -0,0 +1,28 @@ +import logging + +from aiogram.types import Message, ParseMode + +from src.settings import settings + +logger = logging.getLogger() + + +async def debug_message(message: Message, text: str): + """ + Show to users in chat some useful debug info + if debug mode is on. + To toggle debug mode just call `meme admin toggle debug` + + :param message: + :param text: + :return: + """ + logger.debug(text) + if settings.debug_mode: + await message.reply( + "* Debug message: * \n" + f"```\n" + f"{text}" + "```", + parse_mode=ParseMode.MARKDOWN + ) diff --git a/src/utils/decorators.py b/src/utils/decorators.py index 023dad8b57cd70d7d6df618901beb71d055b207a..c106b1129aefbd0c2191aade62f2280f747f270e 100644 --- a/src/utils/decorators.py +++ b/src/utils/decorators.py @@ -4,7 +4,7 @@ from functools import wraps from typing import Callable, List, Tuple from aiogram.dispatcher.filters.state import State -from aiogram.types import Message, ParseMode +from aiogram.types import Message, ParseMode, ReplyKeyboardRemove from src.settings import settings @@ -12,6 +12,14 @@ logger = logging.getLogger() def cool_response_exception(f: Callable): + """ + Wraps message handler to reply to user with + user-friendly message instead of ignoring. + + :param f: wrapped function + :return: Any + """ + @wraps(f) async def decorated_func(message: Message, state=None): try: @@ -19,13 +27,26 @@ def cool_response_exception(f: Callable): return result except Exception as e: await message.reply(f"Command cannot be executed, because of the following error: \n```\n{str(e)}```", - parse_mode=ParseMode.MARKDOWN) + parse_mode=ParseMode.MARKDOWN, + reply_markup=ReplyKeyboardRemove()) logger.exception(e) return decorated_func -def bot_action(trigger_str: str = None, params: List[Tuple] = None, filter_state: State = None): +def bot_action(trigger_str: str = None, params: List[Tuple] = None, filter_state: State = None) -> Callable: + """ + Mark function as bot message handler. + params -> is just an array of tuples. + Each value have regex string and name + + + :param trigger_str: message that triggers bot + :param params: parameters in message + :param filter_state: trigger state if you use Finite State Machine mechanism + :return: decorator + """ + def decor(f: Callable): regexp_params = ' '.join([f'(?P<{name}>{pattern})' for name, pattern in params]) if params else '' filter_regexp = f'^{trigger_str} {regexp_params}'.strip()