From a7a8b4b10d2157c6fae2149058603b1b1d792690 Mon Sep 17 00:00:00 2001 From: Aidaho Date: Mon, 12 Aug 2024 23:41:31 +0300 Subject: [PATCH] v8.0: Delete check_version.html and refactor version checking logic Removed the `check_version.html` template and moved version checking to `script.js`. This refactoring simplifies the codebase by consolidating version check handling into JavaScript, removing deprecated HTML templates, and enhancing the backend logic and API routes. --- app/api/routes/routes.py | 24 +- app/create_db.py | 2 +- app/jobs.py | 1 - app/modules/common/common_classes.py | 1 - app/modules/db/backup.py | 12 +- app/modules/db/cred.py | 6 +- app/modules/db/server.py | 4 +- app/modules/roxywi/class_models.py | 17 +- app/modules/roxywi/roxy.py | 31 +- app/modules/server/ssh.py | 32 +- app/modules/service/backup.py | 109 +++--- app/modules/service/installation.py | 6 +- app/modules/tools/alerting.py | 3 +- app/routes/admin/routes.py | 5 +- app/routes/main/routes.py | 27 +- app/routes/server/routes.py | 32 +- .../ansible/roles/docker/handlers/main.yml | 5 +- app/scripts/ansible/roles/git_backup.yml | 14 +- app/static/js/backup.js | 42 +-- app/static/js/script.js | 15 +- app/templates/ajax/check_version.html | 18 - app/templates/ajax/load_updateroxywi.html | 12 +- app/templates/ajax/new_git.html | 4 +- app/templates/include/admin_ssh.html | 6 +- app/views/server/backup_vews.py | 152 +++++++- app/views/server/cred_views.py | 319 ++++++++++++++++ app/views/server/views.py | 344 ++---------------- requirements.txt | 1 - 28 files changed, 732 insertions(+), 512 deletions(-) delete mode 100644 app/templates/ajax/check_version.html create mode 100644 app/views/server/cred_views.py diff --git a/app/api/routes/routes.py b/app/api/routes/routes.py index 37cd2f8..4f24da3 100644 --- a/app/api/routes/routes.py +++ b/app/api/routes/routes.py @@ -2,13 +2,12 @@ from flask_swagger import swagger from flask import jsonify, render_template, abort from flask_pydantic import validate -from app import app, jwt +from app import app from app.api.routes import bp from app.views.install.views import InstallView -from app.views.server.views import ( - ServerView, CredView, CredsView, ServerGroupView, ServerGroupsView, ServersView, ServerIPView -) -from app.views.server.backup_vews import BackupView, S3BackupView +from app.views.server.views import ServerView, ServerGroupView, ServerGroupsView, ServersView, ServerIPView +from app.views.server.cred_views import CredView, CredsView +from app.views.server.backup_vews import BackupView, S3BackupView, GitBackupView from app.views.service.views import ServiceView, ServiceActionView, ServiceBackendView, ServiceConfigView, ServiceConfigVersionsView from app.views.ha.views import HAView, HAVIPView, HAVIPsView from app.views.user.views import UserView, UserGroupView, UserRoles @@ -30,20 +29,10 @@ def before_request(): pass -@jwt.expired_token_loader -def my_expired_token_callback(jwt_header, jwt_payload): - return jsonify(error="Token is expired"), 401 - - -@jwt.unauthorized_loader -def custom_unauthorized_response(_err): - return jsonify(error="Authorize first"), 401 - - def register_api(view, endpoint, url, pk='listener_id', pk_type='int'): view_func = view.as_view(endpoint) bp.add_url_rule(url, view_func=view_func, methods=['POST']) - bp.add_url_rule(f'{url}/<{pk_type}:{pk}>', view_func=view_func, methods=['GET', 'PUT', 'DELETE']) + bp.add_url_rule(f'{url}/<{pk_type}:{pk}>', view_func=view_func, methods=['GET', 'PUT', 'PATCH', 'DELETE']) def register_api_id_ip(view, endpoint, url: str = '', methods: list = ['GET', 'POST']): @@ -73,9 +62,10 @@ register_api_id_ip(ServiceActionView, 'service_action', '//ip', view_func=ServerIPView.as_view('server_ip_ip'), methods=['GET']) bp.add_url_rule('/server//ip', view_func=ServerIPView.as_view('server_ip'), methods=['GET']) -register_api(CredView, 'cred', '/server/cred', 'creds_id') +register_api(CredView, 'cred', '/server/cred', 'cred_id') bp.add_url_rule('/server/creds', view_func=CredsView.as_view('creds'), methods=['GET']) bp.add_url_rule('/servers', view_func=ServersView.as_view('servers'), methods=['GET']) diff --git a/app/create_db.py b/app/create_db.py index 913c716..2f103ca 100644 --- a/app/create_db.py +++ b/app/create_db.py @@ -147,7 +147,7 @@ def default_values(): print(str(e)) try: - Groups.insert(name='Default', description='All servers are included in this group by default', group_id=1).on_conflict_ignore().execute() + Groups.insert(name='Default', description='All servers are included in this group by default', id=1).on_conflict_ignore().execute() except Exception as e: print(str(e)) diff --git a/app/jobs.py b/app/jobs.py index 371fa3c..259a480 100644 --- a/app/jobs.py +++ b/app/jobs.py @@ -4,7 +4,6 @@ import datetime from app import scheduler import app.modules.db.sql as sql -import app.modules.db.user as user_sql import app.modules.db.roxy as roxy_sql import app.modules.db.history as history_sql import app.modules.roxywi.roxy as roxy diff --git a/app/modules/common/common_classes.py b/app/modules/common/common_classes.py index 8020702..2e0b318 100644 --- a/app/modules/common/common_classes.py +++ b/app/modules/common/common_classes.py @@ -4,7 +4,6 @@ from flask import g import app.modules.db.server as server_sql import app.modules.roxywi.common as roxywi_common -from app.modules.roxywi.exception import RoxywiResourceNotFound from app.modules.roxywi.class_models import ServerRequest, GroupQuery, CredRequest, ChannelRequest from app.middleware import get_user_params diff --git a/app/modules/db/backup.py b/app/modules/db/backup.py index f8bb22d..2dda97c 100644 --- a/app/modules/db/backup.py +++ b/app/modules/db/backup.py @@ -48,9 +48,9 @@ def delete_s3_backups(backup_id: int) -> bool: return True -def insert_new_git(server_id, service_id, repo, branch, period, cred, description) -> None: +def insert_new_git(server_id, service_id, repo, branch, period, cred, description) -> int: try: - GitSetting.insert( + return GitSetting.insert( server_id=server_id, service_id=service_id, repo=repo, branch=branch, period=period, cred_id=cred, description=description ).execute() @@ -58,15 +58,11 @@ def insert_new_git(server_id, service_id, repo, branch, period, cred, descriptio out_error(e) -def delete_git(git_id): - query = GitSetting.delete().where(GitSetting.id == git_id) +def delete_git(git_id: int) -> None: try: - query.execute() + GitSetting.delete().where(GitSetting.id == git_id).execute() except Exception as e: out_error(e) - return False - else: - return True def select_gits(**kwargs): diff --git a/app/modules/db/cred.py b/app/modules/db/cred.py index 9faac5e..c375159 100644 --- a/app/modules/db/cred.py +++ b/app/modules/db/cred.py @@ -4,7 +4,9 @@ from app.modules.roxywi.exception import RoxywiResourceNotFound def select_ssh(**kwargs): - if kwargs.get("name") is not None: + if kwargs.get("group") and kwargs.get("cred_id"): + query = Cred.select().where((Cred.id == kwargs.get('cred_id')) & (Cred.group_id == kwargs.get('group'))) + elif kwargs.get("name") is not None: query = Cred.select().where(Cred.name == kwargs.get('name')) elif kwargs.get("id") is not None: query = Cred.select().where(Cred.id == kwargs.get('id')) @@ -16,6 +18,8 @@ def select_ssh(**kwargs): query = Cred.select() try: query_res = query.execute() + except Cred.DoesNotExist: + raise RoxywiResourceNotFound except Exception as e: out_error(e) else: diff --git a/app/modules/db/server.py b/app/modules/db/server.py index 0829715..5d83c72 100644 --- a/app/modules/db/server.py +++ b/app/modules/db/server.py @@ -25,10 +25,10 @@ def delete_server(server_id): return True -def update_server(hostname, group, type_ip, enable, master, server_id, cred, port, desc, firewall, protected): +def update_server(hostname, ip, group, type_ip, enable, master, server_id, cred, port, desc, firewall, protected): try: server_update = Server.update( - hostname=hostname, group_id=group, type_ip=type_ip, enabled=enable, master=master, cred_id=cred, + hostname=hostname, ip=ip, group_id=group, type_ip=type_ip, enabled=enable, master=master, cred_id=cred, port=port, description=desc, firewall_enable=firewall, protected=protected ).where(Server.server_id == server_id) server_update.execute() diff --git a/app/modules/roxywi/class_models.py b/app/modules/roxywi/class_models.py index 791dde4..815d7de 100644 --- a/app/modules/roxywi/class_models.py +++ b/app/modules/roxywi/class_models.py @@ -4,7 +4,7 @@ from typing import Optional, Annotated, Union, Literal, Any, Dict, List from shlex import quote from pydantic_core import CoreSchema, core_schema -from pydantic import BaseModel, field_validator, StringConstraints, IPvAnyAddress, AnyUrl, root_validator, GetCoreSchemaHandler +from pydantic import BaseModel, Base64Str, StringConstraints, IPvAnyAddress, AnyUrl, root_validator, GetCoreSchemaHandler DomainName = Annotated[str, StringConstraints(pattern=r"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$")] @@ -110,7 +110,7 @@ class ServerRequest(BaseModel): description: Optional[EscapedString] = None group_id: Optional[int] = None protected: Optional[bool] = 0 - master: Optional[int] = None + master: Optional[int] = 0 port: Annotated[int, Gt(1), Le(65535)] = 22 haproxy: Optional[bool] = 0 nginx: Optional[bool] = 0 @@ -137,7 +137,7 @@ class CredRequest(BaseModel): class CredUploadRequest(BaseModel): - private_key: str + private_key: Union[Base64Str, str] passphrase: Optional[EscapedString] = None @@ -252,3 +252,14 @@ class S3BackupRequest(BaseModel): access_key: Optional[EscapedString] = None time: Optional[EscapedString] = None description: Optional[EscapedString] = None + + +class GitBackupRequest(BaseModel): + server_id: int + service_id: int + init: Optional[bool] = 0 + repo: Optional[EscapedString] = None + branch: Optional[EscapedString] = 'main' + time: Optional[EscapedString] = 'weekly' + cred_id: Optional[int] = None + description: Optional[EscapedString] = None diff --git a/app/modules/roxywi/roxy.py b/app/modules/roxywi/roxy.py index 2a0314d..86a3e9d 100644 --- a/app/modules/roxywi/roxy.py +++ b/app/modules/roxywi/roxy.py @@ -1,5 +1,6 @@ import os import re +from packaging import version import distro import requests @@ -26,33 +27,31 @@ def is_docker() -> bool: return True return False + def check_ver(): return roxy_sql.get_ver() def versions(): + json_data = { + 'need_update': 0 + } try: - current_ver = check_ver() - current_ver_without_dots = current_ver.split('.') - current_ver_without_dots = ''.join(current_ver_without_dots) - current_ver_without_dots = current_ver_without_dots.replace('\n', '') - current_ver_without_dots = int(current_ver_without_dots) - except Exception: - current_ver = "Cannot get current version" - current_ver_without_dots = 0 + current_ver = roxy_sql.get_ver() + json_data['current_ver'] = roxy_sql.get_ver() + except Exception as e: + raise Exception(f'Cannot get current version: {e}') try: new_ver = check_new_version('roxy-wi') - new_ver_without_dots = new_ver.split('.') - new_ver_without_dots = ''.join(new_ver_without_dots) - new_ver_without_dots = new_ver_without_dots.replace('\n', '') - new_ver_without_dots = int(new_ver_without_dots) + json_data['new_ver'] = new_ver except Exception as e: - new_ver = "Cannot get a new version" - new_ver_without_dots = 0 - roxywi_common.logging('Roxy-WI server', f' {e}', roxywi=1) + raise Exception(f'Cannot get new version: {e}') + + if version.parse(current_ver) < version.parse(new_ver): + json_data['need_update'] = 1 - return current_ver, new_ver, current_ver_without_dots, new_ver_without_dots + return json_data def check_new_version(service): diff --git a/app/modules/server/ssh.py b/app/modules/server/ssh.py index 997db5d..bafd965 100644 --- a/app/modules/server/ssh.py +++ b/app/modules/server/ssh.py @@ -1,8 +1,10 @@ import os +import base64 from cryptography.fernet import Fernet import paramiko from flask import render_template +from playhouse.shortcuts import model_to_dict import app.modules.db.cred as cred_sql import app.modules.db.group as group_sql @@ -153,7 +155,7 @@ def upload_ssh_key(ssh_id: int, key: str, passphrase: str) -> None: roxywi_common.logging('RMON server', e.args[0], roxywi=1) raise Exception(e) - if passphrase != "''": + if passphrase: try: passphrase = crypt_password(passphrase) except Exception as e: @@ -244,3 +246,31 @@ def decrypt_password(password: str) -> str: except Exception as e: raise Exception(f'error: Cannot decrypt password: {e}') return decryp_pass + + +def get_creds(group_id: int = None, cred_id: int = None) -> list: + json_data = [] + lib_path = get_config.get_config_var('main', 'lib_path') + + if group_id and cred_id: + creds = cred_sql.select_ssh(group=group_id, cred_id=cred_id) + elif group_id: + creds = cred_sql.select_ssh(group=group_id) + else: + creds = cred_sql.select_ssh() + + for cred in creds: + cred_dict = model_to_dict(cred) + cred_dict['name'] = cred_dict['name'].replace("'", "") + ssh_key_file = f'{lib_path}/keys/{cred_dict["name"]}.pem' + if os.path.isfile(ssh_key_file): + with open(ssh_key_file, 'rb') as key: + cred_dict['private_key'] = base64.b64encode(key.read()).decode('utf-8') + else: + cred_dict['private_key'] = '' + if cred_dict['password']: + cred_dict['password'] = decrypt_password(cred_dict['password']) + if cred_dict['passphrase']: + cred_dict['passphrase'] = decrypt_password(cred_dict['passphrase']) + json_data.append(cred_dict) + return json_data diff --git a/app/modules/service/backup.py b/app/modules/service/backup.py index 5d74986..e1307dc 100644 --- a/app/modules/service/backup.py +++ b/app/modules/service/backup.py @@ -1,3 +1,4 @@ +from docutils.parsers.rst.directives import body from flask import render_template import app.modules.db.sql as sql @@ -8,7 +9,7 @@ import app.modules.db.service as service_sql import app.modules.server.ssh as ssh_mod import app.modules.roxywi.common as roxywi_common import app.modules.service.installation as installation_mod -from app.modules.roxywi.class_models import BackupRequest, IdResponse, IdDataResponse, BaseResponse, S3BackupRequest +from app.modules.roxywi.class_models import BackupRequest, IdResponse, IdDataResponse, BaseResponse, S3BackupRequest, GitBackupRequest def create_backup_inv(json_data: BackupRequest, del_id: int = 0) -> None: @@ -51,6 +52,29 @@ def create_s3_backup_inv(data: S3BackupRequest, tag: str) -> None: raise Exception(f'error: {e}') +def create_git_backup_inv(data: GitBackupRequest, server_ip: str, service: str, del_job: int = 0) -> None: + service_config_dir = sql.get_setting(service + '_dir') + ssh_settings = ssh_mod.return_ssh_keys_path(server_ip, id=data.cred_id) + print('del_job',del_job) + inv = {"server": {"hosts": {}}} + inv["server"]["hosts"][server_ip] = { + "REPO": data.repo, + "CONFIG_DIR": service_config_dir, + "PERIOD": data.time, + "INIT": data.init, + "BRANCH": data.branch, + "SERVICE": service, + "DELJOB": del_job, + "KEY": ssh_settings['key'] + } + + try: + installation_mod.run_ansible(inv, [server_ip], 'git_backup') + except Exception as e: + raise Exception(f'error: {e}') + + + def create_backup(json_data: BackupRequest, is_api: bool) -> tuple: if backup_sql.check_exists_backup(json_data.server): raise Exception(f'warning: Backup job for {json_data.server} already exists') @@ -71,7 +95,6 @@ def create_backup(json_data: BackupRequest, is_api: bool) -> tuple: return IdDataResponse(data=data, id=last_id).model_dump(mode='json'), 201 - def delete_backup(json_data: BackupRequest, backup_id: int) -> tuple: create_backup_inv(json_data, backup_id) backup_sql.delete_backups(backup_id) @@ -88,13 +111,6 @@ def update_backup(json_data: BackupRequest, backup_id: int) -> tuple: def create_s3_backup(data: S3BackupRequest, is_api: bool) -> tuple: - # if deljob: - # time = '' - # secret_key = '' - # access_key = '' - # tag = 'delete' - # else: - # tag = 'add' if backup_sql.check_exists_s3_backup(data.server): raise Exception(f'Backup job for {data.server} already exists') @@ -103,7 +119,6 @@ def create_s3_backup(data: S3BackupRequest, is_api: bool) -> tuple: except Exception as e: raise e - # if not deljob: try: last_id = backup_sql.insert_s3_backup_job(**data.model_dump(mode='json')) roxywi_common.logging('backup ', f'a new S3 backup job for server {data.server} has been created', roxywi=1, login=1) @@ -115,11 +130,6 @@ def create_s3_backup(data: S3BackupRequest, is_api: bool) -> tuple: else: temp = render_template('ajax/new_s3_backup.html', backups=backup_sql.select_s3_backups(**data.model_dump(mode='json'))) return IdDataResponse(id=last_id, data=temp).model_dump(mode='json'), 201 - # elif deljob: - # backup_sql.delete_s3_backups(deljob) - # roxywi_common.logging('backup ', f' a S3 backup job for server {server} has been deleted', roxywi=1, login=1) - # return 'ok' - def delete_s3_backup(data: S3BackupRequest, backup_id: int) -> None: @@ -131,46 +141,49 @@ def delete_s3_backup(data: S3BackupRequest, backup_id: int) -> None: raise e -def git_backup(server_id, service_id, git_init, repo, branch, period, cred, del_job, description, backup_id) -> str: - server_ip = server_sql.select_server_ip_by_id(server_id) - service_name = service_sql.select_service_name_by_id(service_id).lower() - service_config_dir = sql.get_setting(service_name + '_dir') - ssh_settings = ssh_mod.return_ssh_keys_path(server_ip, id=cred) - - if repo is None or git_init == '0': - repo = '' - if branch is None or branch == '0': - branch = 'main' - - inv = {"server": {"hosts": {}}} - inv["server"]["hosts"][server_ip] = { - "REPO": repo, - "CONFIG_DIR": service_config_dir, - "PERIOD": period, - "INIT": git_init, - "BRANCH": branch, - "SERVICE": service_name, - "DELJOB": del_job, - "KEY": ssh_settings['key'] - } +def create_git_backup(data: GitBackupRequest, is_api: bool) -> tuple: + server_ip = server_sql.select_server_ip_by_id(data.server_id) + service_name = service_sql.select_service_name_by_id(data.service_id).lower() + try: + create_git_backup_inv(data, server_ip, service_name) + except Exception as e: + raise Exception(e) try: - installation_mod.run_ansible(inv, [server_ip], 'git_backup') + last_id = backup_sql.insert_new_git(server_id=data.server_id, service_id=data.service_id, repo=data.repo, branch=data.branch, period=data.time, + cred=data.cred_id, description=data.description) + roxywi_common.logging(server_ip, 'A new git job has been created', roxywi=1, login=1, keep_history=1, + service=service_name) except Exception as e: - raise Exception(f'error: {e}') + raise Exception(e) - if not del_job: - backup_sql.insert_new_git(server_id=server_id, service_id=service_id, repo=repo, branch=branch, period=period, cred=cred, description=description) + if is_api: + return IdResponse(id=last_id).model_dump(mode='json'), 201 + else: kwargs = { - "gits": backup_sql.select_gits(server_id=server_id, service_id=service_id), - "sshs": cred_sql.select_ssh(), + "gits": backup_sql.select_gits(server_id=data.server_id, service_id=data.service_id), + "sshs": cred_sql.select_ssh(), "servers": roxywi_common.get_dick_permit(), "services": service_sql.select_services(), "new_add": 1, "lang": roxywi_common.get_user_lang_for_flask() } - roxywi_common.logging(server_ip, 'A new git job has been created', roxywi=1, login=1, keep_history=1, service=service_name) - return render_template('ajax/new_git.html', **kwargs) - else: - if backup_sql.delete_git(backup_id): - return 'ok' + + temp = render_template('ajax/new_git.html', **kwargs) + return IdDataResponse(id=last_id, data=temp).model_dump(mode='json'), 201 + + +def delete_git_backup(data: GitBackupRequest, backup_id: int) -> tuple: + server_ip = server_sql.select_server_ip_by_id(data.server_id) + service_name = service_sql.select_service_name_by_id(data.service_id).lower() + try: + create_git_backup_inv(data, server_ip, service_name, 1) + except Exception as e: + raise Exception(e) + + try: + backup_sql.delete_git(backup_id) + except Exception as e: + raise Exception(e) + + return BaseResponse().model_dump(mode='json'), 204 diff --git a/app/modules/service/installation.py b/app/modules/service/installation.py index 0c68178..da3e897 100644 --- a/app/modules/service/installation.py +++ b/app/modules/service/installation.py @@ -1,9 +1,9 @@ import os import json from typing import Union - from packaging import version +import distro import ansible import ansible_runner @@ -212,6 +212,8 @@ def run_ansible(inv: dict, server_ips: list, ansible_role: str) -> dict: proxy = sql.get_setting('proxy') proxy_serv = '' tags = '' + python_int = '/usr/bin/python3' if distro.id() == 'ubuntu' else '/usr/bin/python3.11' + try: agent_pid = server_mod.start_ssh_agent() except Exception as e: @@ -261,7 +263,7 @@ def run_ansible(inv: dict, server_ips: list, ansible_role: str) -> dict: 'AWX_DISPLAY': False, 'SSH_AUTH_PID': agent_pid['pid'], 'SSH_AUTH_SOCK': agent_pid['socket'], - 'ANSIBLE_PYTHON_INTERPRETER': '/usr/bin/python3' + 'ANSIBLE_PYTHON_INTERPRETER': python_int } kwargs = { 'private_data_dir': '/var/www/haproxy-wi/app/scripts/ansible/', diff --git a/app/modules/tools/alerting.py b/app/modules/tools/alerting.py index 9528d7a..db9e26b 100644 --- a/app/modules/tools/alerting.py +++ b/app/modules/tools/alerting.py @@ -171,6 +171,7 @@ def send_email(email_to: str, subject: str, message: str) -> None: def telegram_send_mess(mess, level, **kwargs): token_bot = '' channel_name = '' + proxy = sql.get_setting('proxy') if kwargs.get('channel_id') == 0: return @@ -180,8 +181,6 @@ def telegram_send_mess(mess, level, **kwargs): else: telegrams = channel_sql.get_receiver_by_ip('telegram', kwargs.get('ip')) - proxy = sql.get_setting('proxy') - for telegram in telegrams: token_bot = telegram.token channel_name = telegram.chanel_name diff --git a/app/routes/admin/routes.py b/app/routes/admin/routes.py index 72b9b21..b6330c0 100644 --- a/app/routes/admin/routes.py +++ b/app/routes/admin/routes.py @@ -16,6 +16,7 @@ import app.modules.roxywi.auth as roxywi_auth import app.modules.roxywi.common as roxywi_common import app.modules.tools.smon as smon_mod import app.modules.tools.common as tools_common +import app.modules.server.ssh as ssh_mod from app.views.admin.views import SettingsView bp.add_url_rule( @@ -41,12 +42,12 @@ def admin(): users = user_sql.select_users() servers = server_sql.select_servers(full=1) masters = server_sql.select_servers(get_master_servers=1) - sshs = cred_sql.select_ssh() + sshs = ssh_mod.get_creds() else: users = user_sql.select_users(group=user_group) servers = roxywi_common.get_dick_permit(virt=1, disable=0, only_group=1) masters = server_sql.select_servers(get_master_servers=1, uuid=g.user_params['user_id']) - sshs = cred_sql.select_ssh(group=user_group) + sshs = ssh_mod.get_creds(group_id=user_group) kwargs = { 'lang': g.user_params['lang'], diff --git a/app/routes/main/routes.py b/app/routes/main/routes.py index 67d3e45..e52ee7f 100644 --- a/app/routes/main/routes.py +++ b/app/routes/main/routes.py @@ -4,7 +4,7 @@ from flask import render_template, request, g, abort, jsonify, redirect, url_for from flask_jwt_extended import jwt_required from flask_pydantic.exceptions import ValidationError -from app import app, cache +from app import app, cache, jwt from app.routes.main import bp import app.modules.db.user as user_sql import app.modules.db.server as server_sql @@ -26,6 +26,16 @@ def _jinja2_filter_datetime(date, fmt=None): return common.get_time_zoned_date(date, fmt) +@jwt.expired_token_loader +def my_expired_token_callback(jwt_header, jwt_payload): + return jsonify(error="Token is expired"), 401 + + +@jwt.unauthorized_loader +def custom_unauthorized_response(_err): + return jsonify(error="Authorize first"), 401 + + @app.errorhandler(ValidationError) def handle_pydantic_validation_errors1(e): errors = [] @@ -70,6 +80,7 @@ def page_is_forbidden(e): def page_not_found(e): if 'api' in request.url: return jsonify({'error': str(e)}), 404 + get_user_params() kwargs = { 'user_params': g.user_params, 'title': e, @@ -79,23 +90,15 @@ def page_not_found(e): @app.errorhandler(405) -@get_user_params() def method_not_allowed(e): - if 'api' in request.url: - return jsonify({'error': str(e)}), 405 - kwargs = { - 'user_params': g.user_params, - 'title': e, - 'e': e - } - return render_template('error.html', **kwargs), 405 + return jsonify({'error': str(e)}), 405 @app.errorhandler(500) -@get_user_params() def internal_error(e): if 'api' in request.url: return jsonify({'error': str(e)}), 500 + get_user_params() kwargs = { 'user_params': g.user_params, 'title': e, @@ -224,4 +227,4 @@ def service_history(service, server_ip): @bp.route('/internal/show_version') @cache.cached() def show_roxywi_version(): - return render_template('ajax/check_version.html', versions=roxy.versions()) + return jsonify(roxy.versions()) diff --git a/app/routes/server/routes.py b/app/routes/server/routes.py index 89044c4..5d87105 100644 --- a/app/routes/server/routes.py +++ b/app/routes/server/routes.py @@ -7,14 +7,15 @@ from app.routes.server import bp import app.modules.db.cred as cred_sql import app.modules.db.server as server_sql import app.modules.db.backup as backup_sql +import app.modules.db.service as service_sql import app.modules.common.common as common import app.modules.roxywi.auth as roxywi_auth import app.modules.roxywi.common as roxywi_common import app.modules.server.server as server_mod -import app.modules.service.backup as backup_mod from app.middleware import get_user_params -from app.views.server.views import ServerView, CredView, CredsView, ServerGroupView, ServerGroupsView, ServerIPView -from app.views.server.backup_vews import BackupView, S3BackupView +from app.views.server.views import ServerView, ServerGroupView, ServerGroupsView, ServerIPView +from app.views.server.cred_views import CredView, CredsView +from app.views.server.backup_vews import BackupView, S3BackupView, GitBackupView def register_api(view, endpoint, url, pk='listener_id', pk_type='int'): @@ -33,6 +34,7 @@ bp.add_url_rule('//ip', view_func=ServerIPView.as_view('server_ip_ip' bp.add_url_rule('//ip', view_func=ServerIPView.as_view('server_ip'), methods=['GET']) bp.add_url_rule('/backup', view_func=BackupView.as_view('backup', False), methods=['POST']) bp.add_url_rule('/backup/s3', view_func=S3BackupView.as_view('backup_s3', False), methods=['POST']) +bp.add_url_rule('/backup/git', view_func=GitBackupView.as_view('backup_git', False), methods=['POST']) error_mess = roxywi_common.return_error_message() @@ -137,6 +139,7 @@ def load_backup(): kwargs = { 'sshs': cred_sql.select_ssh(group=user_group), 'servers': roxywi_common.get_dick_permit(virt=1, disable=0, only_group=1), + 'services': service_sql.select_services(), 'backups': backup_sql.select_backups(), 's3_backups': backup_sql.select_s3_backups(), 'gits': backup_sql.select_gits(), @@ -145,26 +148,3 @@ def load_backup(): 'user_subscription': roxywi_common.return_user_subscription(), } return render_template('include/admin_backup.html', **kwargs) - - -@bp.route('/git', methods=['DELETE', 'POST']) -def create_git_backup(): - json_data = request.get_json() - server_id = int(json_data['server']) - service_id = int(json_data['service']) - git_init = int(json_data['init']) - repo = common.checkAjaxInput(json_data['repo']) - branch = common.checkAjaxInput(json_data['branch']) - period = common.checkAjaxInput(json_data['time']) - cred = int(json_data['cred']) - del_job = int(json_data['del_job']) - description = common.checkAjaxInput(json_data['desc']) - backup_id = '' - if request.method == 'DELETE': - backup_id = json_data['backup_id'] - - try: - data = backup_mod.git_backup(server_id, service_id, git_init, repo, branch, period, cred, del_job, description, backup_id) - return jsonify({'status': 'ok', 'data': data}) - except Exception as e: - return roxywi_common.handle_json_exceptions(e, f'Cannot {request.method} git backup') diff --git a/app/scripts/ansible/roles/docker/handlers/main.yml b/app/scripts/ansible/roles/docker/handlers/main.yml index 537e428..20b58ab 100644 --- a/app/scripts/ansible/roles/docker/handlers/main.yml +++ b/app/scripts/ansible/roles/docker/handlers/main.yml @@ -1,4 +1,7 @@ --- +- name: restart rsyslog + service: name=rsyslog state=restarted + - name: restart docker service: "name=docker state={{ docker_restart_handler_state }}" - ignore_errors: "{{ ansible_check_mode }}" \ No newline at end of file + ignore_errors: "{{ ansible_check_mode }}" diff --git a/app/scripts/ansible/roles/git_backup.yml b/app/scripts/ansible/roles/git_backup.yml index bf649c3..917f767 100644 --- a/app/scripts/ansible/roles/git_backup.yml +++ b/app/scripts/ansible/roles/git_backup.yml @@ -11,7 +11,7 @@ - name: Fail if has been installed fail: msg="Git configuration not found. Initialize git at the beginning" - when: not register_name.stat.exists and not INIT and DELJOB + when: not register_name.stat.exists and not INIT and not DELJOB - name: Install git package: @@ -34,15 +34,16 @@ - name: Copy ssh file copy: src: '{{ KEY }}' - dest: '/home/{{ ansible_user }}/.ssh/id_rsa' + dest: '/home/{{ ansible_user }}/.ssh/git_{{ SERVICE }}_key' mode: 0600 group: '{{ ansible_user }}' owner: '{{ ansible_user }}' - force: no + force: yes when: INIT - name: Add write permissions shell: "chmod o+wr -R {{ CONFIG_DIR }}" + when: not DELJOB - name: Git init shell: 'cd {{ CONFIG_DIR }} && git init' @@ -58,13 +59,18 @@ email = roxy-wi@.com when: INIT + - name: Add dir exception + shell: 'git config --global --add safe.directory {{ CONFIG_DIR }}' + when: INIT + become: no + - name: Git add remote shell: 'cd {{ CONFIG_DIR }} && git add --all . && git commit -m "Roxy-WI init repo" && git branch -M {{ BRANCH }} && git remote add origin {{ REPO }}' when: INIT become: no - name: Git add push - shell: 'cd {{ CONFIG_DIR }} && GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no" git push -u origin {{ BRANCH }}' + shell: 'cd {{ CONFIG_DIR }} && GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -i /home/{{ ansible_user }}/.ssh/git_{{ SERVICE }}_key" git push -u origin {{ BRANCH }}' when: INIT become: no diff --git a/app/static/js/backup.js b/app/static/js/backup.js index 9ce7f11..2e309aa 100644 --- a/app/static/js/backup.js +++ b/app/static/js/backup.js @@ -219,18 +219,17 @@ function addGit(dialog_id) { valid = valid && checkLength(branch_div, "Branch name", 1); if (valid) { let jsonData = { - "server": server_div.val(), - "service": service_div.val(), + "server_id": server_div.val(), + "service_id": service_div.val(), "init": git_init, "repo": $('#git-repo').val(), "branch": branch_div.val(), "time": time_div.val(), - "cred": cred_div.val(), - "del_job": 0, + "cred_id": cred_div.val(), "desc": $('#git-description').text(), } $.ajax({ - url: "/server/git", + url: "/server/backup/git", data: JSON.stringify(jsonData), contentType: "application/json; charset=utf-8", type: "POST", @@ -388,34 +387,35 @@ function removeS3Backup(id) { function removeGit(id) { $("#git-table-" + id).css("background-color", "#f2dede"); let jsonData = { - "backup_id": id, - "del_job": 1, - "init": 0, - "repo": 0, - "branch": 0, - "time": 0, - "cred": $('#git-credentials-id-' + id).text(), - "server": $('#git-server-id-' + id).text(), - "service": $('#git-service-id-' + id).text(), - "desc": '', + "cred_id": $('#git-credentials-id-' + id).text(), + "server_id": $('#git-server-id-' + id).text(), + "service_id": $('#git-service-id-' + id).text(), } $.ajax({ - url: "/server/git", + url: api_prefix + "/server/backup/git/" + id, data: JSON.stringify(jsonData), contentType: "application/json; charset=utf-8", type: "DELETE", - success: function (data) { - if (data.status === 'failed') { - toastr.error(data.error); - } else { + statusCode: { + 204: function (xhr) { + $("#git-table-" + id).remove(); + }, + 404: function (xhr) { $("#git-table-" + id).remove(); } + }, + success: function (data) { + if (data) { + if (data.status === "failed") { + toastr.error(data); + } + } } }); } function updateBackup(id) { toastr.clear(); - if ($("#backup-type-" + id + " option:selected").val() == "-------" || $('#backup-rserver-' + id).val() == '' || $('#backup-rpath-' + id).val() == '') { + if ($("#backup-type-" + id + " option:selected").val() === "-------" || $('#backup-rserver-' + id).val() === '' || $('#backup-rpath-' + id).val() === '') { toastr.error('All fields must be completed'); } else { let jsonData = { diff --git a/app/static/js/script.js b/app/static/js/script.js index 09efb98..1e3faec 100644 --- a/app/static/js/script.js +++ b/app/static/js/script.js @@ -1163,21 +1163,26 @@ function show_version() { NProgress.configure({showSpinner: false}); $.ajax( { url: "/internal/show_version", + contentType: "application/json; charset=utf-8", success: function( data ) { - $('#version').html(data); - var showUpdates = $( "#show-updates" ).dialog({ + if (data.need_update) { + $('#version').html('v' + data.current_ver + ''); + } else { + $('#version').html('v' + data.current_ver); + } + let showUpdates = $("#show-updates").dialog({ autoOpen: false, width: 600, modal: true, title: 'There is a new Roxy-WI version', buttons: { - Close: function() { - $( this ).dialog( "close" ); + Close: function () { + $(this).dialog("close"); clearTips(); } } }); - $('#show-updates-button').click(function() { + $('#show-updates-button').click(function () { showUpdates.dialog('open'); }); } diff --git a/app/templates/ajax/check_version.html b/app/templates/ajax/check_version.html deleted file mode 100644 index 70edffa..0000000 --- a/app/templates/ajax/check_version.html +++ /dev/null @@ -1,18 +0,0 @@ -{%- if versions is defined -%} - {%- set current_ver = versions.0 -%} - {%- set new_ver = versions.1 %} - {%- set current_ver_without_dots = versions.2 %} - {%- set new_ver_without_dots = versions.3 %} -{%- endif -%} -{%- if new_ver_without_dots is defined and current_ver_without_dots is defined and new_ver is defined and new_ver_without_dots is defined -%} - - {%- if new_ver_without_dots > current_ver_without_dots and new_ver != "Sorry cannot get current version" %} - v{{current_ver}} - - {%- else %} - v{{current_ver}} - {%- endif %} - -{%- else %} - v{{current_ver}} -{% endif -%} diff --git a/app/templates/ajax/load_updateroxywi.html b/app/templates/ajax/load_updateroxywi.html index c0c94cb..777c7bf 100644 --- a/app/templates/ajax/load_updateroxywi.html +++ b/app/templates/ajax/load_updateroxywi.html @@ -1,8 +1,4 @@ {% import 'languages/'+lang|default('en')+'.html' as lang %} -{% set current_ver = versions.0 %} -{% set new_ver = versions.1 %} -{% set current_ver_without_dots = versions.2 %} -{% set new_ver_without_dots = versions.3 %} {% set services_name = { 'roxy-wi-checker': { 'link': 'checker', 'name': 'Checker', 'desc': lang.admin_page.desc.checker_desc }, 'roxy-wi-keep_alive': { 'link': 'auto_start', 'name': 'Auto start', 'desc': lang.admin_page.desc.auto_start_desc }, @@ -19,7 +15,7 @@ current_ver_without_dots and new_ver != "Sorry cannot get current version" %} + {% if versions.need_update %} title=lang.admin_page.desc.a_new_version+" Roxy-WI" style="color: var(--red-color)" {% else %} @@ -27,14 +23,14 @@ style="color: var(--green-color)" {% endif %} > - {{current_ver}} + {{versions.current_ver}} - {{new_ver}} + {{versions.new_ver}} - {% if new_ver_without_dots > current_ver_without_dots and new_ver != "Sorry cannot get current version" %} + {% if versions.need_update %} {{lang.words.w_update|title()}} {% endif %} diff --git a/app/templates/ajax/new_git.html b/app/templates/ajax/new_git.html index 8d59063..9a6ed34 100644 --- a/app/templates/ajax/new_git.html +++ b/app/templates/ajax/new_git.html @@ -38,7 +38,7 @@ {% for ssh in sshs %} - {% if ssh.enable == 1 %} + {% if ssh.key_enabled == 1 %} {% if ssh.id == b.cred_id %} {{ ssh.name }} @@ -47,7 +47,7 @@ {% endfor %} - + {% if b.description %} {{b.description}} {% endif %} diff --git a/app/templates/include/admin_ssh.html b/app/templates/include/admin_ssh.html index 2e7d3cd..8f5b289 100644 --- a/app/templates/include/admin_ssh.html +++ b/app/templates/include/admin_ssh.html @@ -20,7 +20,7 @@ {% set id = 'ssh_name-' + ssh.id|string() %} - {{ input(id, value=ssh.name, size='15') }} + {{ input(id, value=ssh.name.replace("'", ""), size='15') }} {% if ssh.key_enabled == 1 %} @@ -48,9 +48,9 @@ {{ input(id, value=ssh.username, title='SSH user name') }}

{% if ssh.key_enabled == 1 %} - + {% else %} - + {% endif %}
diff --git a/app/views/server/backup_vews.py b/app/views/server/backup_vews.py index 0ee0f28..823c3be 100644 --- a/app/views/server/backup_vews.py +++ b/app/views/server/backup_vews.py @@ -8,7 +8,7 @@ import app.modules.db.backup as backup_sql import app.modules.service.backup as backup_mod import app.modules.roxywi.common as roxywi_common from app.middleware import get_user_params, page_for_admin, check_group -from app.modules.roxywi.class_models import BackupRequest, S3BackupRequest, BaseResponse +from app.modules.roxywi.class_models import BackupRequest, S3BackupRequest, GitBackupRequest, BaseResponse class BackupView(MethodView): @@ -209,7 +209,7 @@ class BackupView(MethodView): class S3BackupView(MethodView): - methods = ['GET', 'POST', 'PUT', 'DELETE'] + methods = ['GET', 'POST', 'DELETE'] decorators = [jwt_required(), get_user_params(), page_for_admin(), check_group()] def __init__(self, is_api=True): @@ -352,3 +352,151 @@ class S3BackupView(MethodView): return BaseResponse().model_dump(mode='json'), 204 except Exception as e: return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot delete S3 backup') + + +class GitBackupView(MethodView): + methods = ['GET', 'POST', 'DELETE'] + decorators = [jwt_required(), get_user_params(), page_for_admin(), check_group()] + + def __init__(self, is_api=True): + self.is_api = is_api + + @staticmethod + def get(backup_id: int): + """ + Retrieves the details of a specific Git backup configuration. + --- + tags: + - Git Backup + parameters: + - in: path + name: backup_id + type: 'integer' + required: true + description: The ID of the specific Git backup + responses: + 200: + description: Successful operation + schema: + type: 'object' + properties: + branch: + type: 'string' + description: 'The branch the backup is on' + cred_id: + type: 'integer' + description: 'The ID of the credentials used for the backup' + description: + type: 'string' + description: 'Description for the Git backup configuration' + id: + type: 'integer' + description: 'The ID of the backup' + period: + type: 'string' + description: 'The timing for the Git backup task' + repo: + type: 'string' + description: 'The repository URL for the backup' + server_id: + type: 'integer' + description: 'The ID of the server that was backed up' + service_id: + type: 'integer' + description: 'The service ID of the backup' + default: + description: Unexpected error + """ + try: + backup = backup_sql.get_backup(backup_id, 'git') + except Exception as e: + return roxywi_common.handler_exceptions_for_json_data(e, '') + + return jsonify(model_to_dict(backup, recurse=False)) + + @validate(body=GitBackupRequest) + def post(self, body: GitBackupRequest): + """ + Create a new Git backup. + --- + tags: + - Git Backup + parameters: + - name: config + in: body + required: true + description: The configuration for Git backup service + schema: + type: 'object' + properties: + server_id: + type: 'integer' + description: 'The ID of the server to backed up' + service_id: + type: 'integer' + description: 'Service ID' + init: + type: 'integer' + description: 'Indicates whether to initialize the repository' + repo: + type: 'string' + description: 'The repository from where to fetch the data for backup' + branch: + type: 'string' + description: 'The branch to pull for backup' + time: + type: 'string' + description: 'The timing for the Git backup task' + cred_id: + type: 'integer' + description: 'The ID of the credentials to be used for backup' + description: + type: 'string' + description: 'Description for the Git backup configuration' + responses: + 201: + description: Successful operation + default: + description: Unexpected error + """ + try: + return backup_mod.create_git_backup(body, self.is_api) + except Exception as e: + return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot create GIT backup') + + @validate(body=GitBackupRequest) + def delete(self, backup_id: int, body: GitBackupRequest): + """ + Deletes a specific Git based backup configuration. + --- + tags: + - Git Backup + parameters: + - in: path + name: backup_id + type: 'integer' + required: true + description: The ID of the specific Git backup + - name: config + in: body + required: true + description: The configuration for Git backup service delete operation + schema: + type: 'object' + properties: + server_id: + type: 'integer' + description: 'ID of the server from where the backup is to be deleted' + service_id: + type: 'integer' + description: 'Service ID of the backup to be deleted' + responses: + 204: + description: Successful operation + default: + description: Unexpected error + """ + try: + return backup_mod.delete_git_backup(body, backup_id) + except Exception as e: + return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot delete GIT backup') diff --git a/app/views/server/cred_views.py b/app/views/server/cred_views.py new file mode 100644 index 0000000..ccc934e --- /dev/null +++ b/app/views/server/cred_views.py @@ -0,0 +1,319 @@ +import base64 + +from flask.views import MethodView +from flask_pydantic import validate +from flask import jsonify, g +from flask_jwt_extended import jwt_required + +import app.modules.db.cred as cred_sql +import app.modules.roxywi.common as roxywi_common +import app.modules.server.ssh as ssh_mod +from app.middleware import get_user_params, page_for_admin, check_group +from app.modules.roxywi.exception import RoxywiGroupMismatch, RoxywiResourceNotFound +from app.modules.roxywi.class_models import BaseResponse, GroupQuery, CredRequest, CredUploadRequest +from app.modules.common.common_classes import SupportClass + +class CredView(MethodView): + methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] + decorators = [jwt_required(), get_user_params(), page_for_admin(level=2), check_group()] + + def __init__(self, is_api=False): + self.is_api = is_api + + @staticmethod + @validate(query=GroupQuery) + def get(cred_id: int, query: GroupQuery): + """ + Retrieve credential information for a specific ID + --- + tags: + - 'SSH credentials' + parameters: + - in: 'path' + name: 'cred_id' + description: 'ID of the credential to retrieve' + required: true + type: 'integer' + responses: + 200: + description: 'Individual Credential Information' + schema: + type: 'object' + properties: + group_id: + type: 'integer' + description: 'Group ID the credential belongs to' + id: + type: 'integer' + description: 'Credential ID' + key_enabled: + type: 'integer' + description: 'Key status of the credential' + name: + type: 'string' + description: 'Name of the credential' + username: + type: 'string' + description: 'Username associated with the credential' + password: + type: 'string' + description: 'Password associated with the credential' + passphrase: + type: 'string' + description: 'Password for the SSH private key' + private_key: + type: 'string' + description: 'SSH private key in base64 encoded format' + 404: + description: 'Credential not found' + """ + group_id = SupportClass.return_group_id(query) + try: + creds = ssh_mod.get_creds(group_id=group_id, cred_id=cred_id) + return jsonify(creds), 200 + except Exception as e: + return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get credentials') + + @validate(body=CredRequest) + def post(self, body: CredRequest): + """ + Create a new credential entry + --- + tags: + - SSH credentials + parameters: + - in: 'path' + name: 'creds_id' + description: 'ID of the credential to retrieve' + required: true + type: 'integer' + - in: body + name: body + schema: + id: AddCredentials + required: + - group_шв + - name + - username + - key_enabled + - password + properties: + group_id: + type: integer + description: The ID of the group to create the credential for. Only for superAdmin role + name: + type: string + description: The credential name + username: + type: string + description: The username + key_enabled: + type: integer + description: If key is enabled or not + password: + type: string + description: The password + responses: + 201: + description: Credential addition successful + """ + group_id = SupportClass.return_group_id(body) + try: + return ssh_mod.create_ssh_cred(body.name, body.password, group_id, body.username, body.key_enabled, self.is_api) + except Exception as e: + return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot create new cred') + + @validate(body=CredRequest) + def put(self, creds_id: int, body: CredRequest): + """ + Update a credential entry + --- + tags: + - SSH credentials + parameters: + - in: 'path' + name: 'creds_id' + description: 'ID of the credential to retrieve' + required: true + type: 'integer' + - in: body + name: body + schema: + id: UpdateCredentials + required: + - name + - username + - key_enabled + - password + properties: + group_id: + type: integer + description: The ID of the group to create the credential for. Only for superAdmin role + name: + type: string + description: The credential name + username: + type: string + description: The username + key_enabled: + type: integer + description: If key is enabled or not + password: + type: string + description: The password + responses: + 201: + description: Credential update successful + """ + group_id = SupportClass.return_group_id(body) + try: + self._check_is_correct_group(creds_id) + except Exception as e: + return roxywi_common.handler_exceptions_for_json_data(e, ''), 404 + + try: + ssh_mod.update_ssh_key(creds_id, body.name, body.password, body.key_enabled, body.username, group_id) + return BaseResponse().model_dump(mode='json'), 201 + except Exception as e: + return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot update SSH key') + + def delete(self, creds_id: int): + """ + Delete a credential entry + --- + tags: + - SSH credentials + parameters: + - in: 'path' + name: 'creds_id' + description: 'ID of the credential to retrieve' + required: true + type: 'integer' + responses: + 204: + description: Credential deletion successful + """ + try: + self._check_is_correct_group(creds_id) + except Exception as e: + return roxywi_common.handler_exceptions_for_json_data(e, ''), 404 + + try: + ssh_mod.delete_ssh_key(creds_id) + return BaseResponse().model_dump(mode='json'), 204 + except Exception as e: + return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot delete SSH key') + + @validate(body=CredUploadRequest) + def patch(self, creds_id: int, body: CredUploadRequest): + """ + Upload an SSH private key + --- + tags: + - SSH credentials + parameters: + - in: 'path' + name: 'creds_id' + description: 'ID of the credential to retrieve' + required: true + type: 'integer' + - in: body + name: body + schema: + id: UploadSSHKey + required: + - private_key + - passphrase + properties: + private_key: + type: string + description: The private key string or base64 encoded string + passphrase: + type: string + description: The passphrase + responses: + 201: + description: SSH key upload successful + """ + try: + self._check_is_correct_group(creds_id) + except Exception as e: + return roxywi_common.handler_exceptions_for_json_data(e, ''), 404 + try: + body.private_key = base64.b64decode(body.private_key).decode("ascii") + except Exception: + pass + try: + ssh_mod.upload_ssh_key(creds_id, body.private_key, body.passphrase) + return BaseResponse().model_dump(mode='json'), 201 + except Exception as e: + return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot upload SSH key') + + @staticmethod + def _check_is_correct_group(creds_id: int): + if g.user_params['role'] == 1: + return True + try: + ssh = cred_sql.get_ssh(creds_id) + except RoxywiResourceNotFound: + raise RoxywiResourceNotFound + if ssh.group_id != g.user_params['group_id']: + raise RoxywiGroupMismatch + + +class CredsView(MethodView): + methods = ['GET'] + decorators = [jwt_required(), get_user_params(), page_for_admin(level=2), check_group()] + + @validate(query=GroupQuery) + def get(self, query: GroupQuery): + """ + Retrieve credential information based on group_id + --- + tags: + - 'SSH credentials' + parameters: + - in: 'query' + name: 'group_id' + description: 'GroupQuery to filter servers. Only for superAdmin role' + required: false + type: 'integer' + responses: + 200: + description: 'Credentials Information' + schema: + type: 'array' + items: + type: 'object' + properties: + group_id: + type: 'integer' + description: 'Group ID the credential belongs to' + id: + type: 'integer' + description: 'Credential ID' + key_enabled: + type: 'integer' + description: 'Key status of the credential' + name: + type: 'string' + description: 'Name of the credential' + username: + type: 'string' + description: 'Username of the credential' + password: + type: 'string' + description: 'Password associated with the credential' + passphrase: + type: 'string' + description: 'Password for the SSH private key' + private_key: + type: 'string' + description: 'SSH private key in base64 encoded format' + """ + group_id = SupportClass.return_group_id(query) + try: + creds = ssh_mod.get_creds(group_id=group_id) + return jsonify(creds), 200 + except Exception as e: + return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get credentials') diff --git a/app/views/server/views.py b/app/views/server/views.py index 6750a34..8a956d7 100644 --- a/app/views/server/views.py +++ b/app/views/server/views.py @@ -1,19 +1,17 @@ from flask.views import MethodView from flask_pydantic import validate -from flask import render_template, jsonify, request, g +from flask import render_template, jsonify, request from playhouse.shortcuts import model_to_dict from flask_jwt_extended import jwt_required -from app.modules.db.db_model import Cred import app.modules.db.cred as cred_sql import app.modules.db.group as group_sql import app.modules.db.server as server_sql import app.modules.roxywi.group as group_mod import app.modules.roxywi.common as roxywi_common -import app.modules.server.ssh as ssh_mod import app.modules.server.server as server_mod from app.middleware import get_user_params, page_for_admin, check_group -from app.modules.roxywi.exception import RoxywiGroupMismatch, RoxywiResourceNotFound +from app.modules.roxywi.exception import RoxywiResourceNotFound from app.modules.roxywi.class_models import ( BaseResponse, IdResponse, IdDataResponse, ServerRequest, GroupQuery, GroupRequest, CredRequest, CredUploadRequest ) @@ -132,21 +130,20 @@ class ServerView(MethodView): required: - hostname - ip - - enabled - - creds_id + - cred_id - port - - description + - group_id properties: hostname: type: string description: The server name ip: type: string - description: The server IP address + description: The server IP address or domain name enabled: type: integer description: If server is enabled or not - creds_id: + cred_id: type: integer description: The ID of the credentials port: @@ -158,6 +155,18 @@ class ServerView(MethodView): group_id: type: integer description: The ID of the group to create the server for. Only for superAdmin role + type_ip: + type: integer + description: Is server virtual (VIP address) or not + master: + type: integer + description: Server id of the master server + firewall_enable: + type: integer + description: Is firewalld enabled or not + protected: + type: integer + description: Is the server protected from changes by a non-admin role responses: 201: description: Server creation successful @@ -216,19 +225,22 @@ class ServerView(MethodView): schema: id: UpdateServer required: - - name - - enabled - - creds_id + - hostname + - ip + - cred_id - port - - description + - group_id properties: - name: + hostname: type: string description: The server name + ip: + type: string + description: The server IP or domain name enabled: type: integer description: If server is enabled or not - creds_id: + cred_id: type: integer description: The ID of the credentials port: @@ -240,6 +252,18 @@ class ServerView(MethodView): group_id: type: integer description: The ID of the group to update the server for. Only for superAdmin role + type_ip: + type: integer + description: Is server virtual (VIP address) or not + master: + type: integer + description: Server id of the master server + firewall_enable: + type: integer + description: Is firewalld enabled or not + protected: + type: integer + description: Is the server protected from changes by a non-admin role responses: 201: description: Server update successful @@ -248,7 +272,7 @@ class ServerView(MethodView): try: server_sql.update_server( - body.hostname, group_id, body.type_ip, body.enabled, body.master, server_id, body.cred_id, body.port, body.description, + body.hostname, body.ip, group_id, body.type_ip, body.enabled, body.master, server_id, body.cred_id, body.port, body.description, body.firewall_enable, body.protected ) server_ip = server_sql.select_server_ip_by_id(server_id) @@ -570,294 +594,6 @@ class ServerGroupsView(MethodView): return jsonify(groups_list) -class CredView(MethodView): - methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] - decorators = [jwt_required(), get_user_params(), page_for_admin(level=2), check_group()] - - def __init__(self, is_api=False): - self.is_api = is_api - - @staticmethod - def get(creds_id: int): - """ - Retrieve credential information for a specific ID - --- - tags: - - 'SSH credentials' - parameters: - - in: 'path' - name: 'creds_id' - description: 'ID of the credential to retrieve' - required: true - type: 'integer' - responses: - 200: - description: 'Individual Credential Information' - schema: - type: 'object' - properties: - group_id: - type: 'integer' - description: 'Group ID the credential belongs to' - id: - type: 'integer' - description: 'Credential ID' - key_enabled: - type: 'integer' - description: 'Key status of the credential' - name: - type: 'string' - description: 'Name of the credential' - username: - type: 'string' - description: 'Username associated with the credential' - 404: - description: 'Credential not found' - """ - group_id = int(g.user_params['group_id']) - try: - creds = cred_sql.get_ssh_by_id_and_group(creds_id, group_id) - for cred in creds: - return jsonify(model_to_dict(cred, exclude=[Cred.password, Cred.passphrase])), 200 - except Exception as e: - return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get credentials') - - @validate(body=CredRequest) - def post(self, body: CredRequest): - """ - Create a new credential entry - --- - tags: - - SSH credentials - parameters: - - in: 'path' - name: 'creds_id' - description: 'ID of the credential to retrieve' - required: true - type: 'integer' - - in: body - name: body - schema: - id: AddCredentials - required: - - group_шв - - name - - username - - key_enabled - - password - properties: - group_id: - type: integer - description: The ID of the group to create the credential for. Only for superAdmin role - name: - type: string - description: The credential name - username: - type: string - description: The username - key_enabled: - type: integer - description: If key is enabled or not - password: - type: string - description: The password - responses: - 201: - description: Credential addition successful - """ - group_id = SupportClass.return_group_id(body) - try: - return ssh_mod.create_ssh_cred(body.name, body.password, group_id, body.username, body.key_enabled, self.is_api) - except Exception as e: - return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot create new cred') - - @validate(body=CredRequest) - def put(self, creds_id: int, body: CredRequest): - """ - Update a credential entry - --- - tags: - - SSH credentials - parameters: - - in: 'path' - name: 'creds_id' - description: 'ID of the credential to retrieve' - required: true - type: 'integer' - - in: body - name: body - schema: - id: UpdateCredentials - required: - - name - - username - - key_enabled - - password - properties: - group_id: - type: integer - description: The ID of the group to create the credential for. Only for superAdmin role - name: - type: string - description: The credential name - username: - type: string - description: The username - key_enabled: - type: integer - description: If key is enabled or not - password: - type: string - description: The password - responses: - 201: - description: Credential update successful - """ - group_id = SupportClass.return_group_id(body) - try: - self._check_is_correct_group(creds_id) - except Exception as e: - return roxywi_common.handler_exceptions_for_json_data(e, ''), 404 - - try: - ssh_mod.update_ssh_key(creds_id, body.name, body.password, body.key_enabled, body.username, group_id) - return BaseResponse().model_dump(mode='json'), 201 - except Exception as e: - return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot update SSH key') - - def delete(self, creds_id: int): - """ - Delete a credential entry - --- - tags: - - SSH credentials - parameters: - - in: 'path' - name: 'creds_id' - description: 'ID of the credential to retrieve' - required: true - type: 'integer' - responses: - 204: - description: Credential deletion successful - """ - try: - self._check_is_correct_group(creds_id) - except Exception as e: - return roxywi_common.handler_exceptions_for_json_data(e, ''), 404 - - try: - ssh_mod.delete_ssh_key(creds_id) - return BaseResponse().model_dump(mode='json'), 204 - except Exception as e: - return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot delete SSH key') - - @validate(body=CredUploadRequest) - def patch(self, creds_id: int, body: CredUploadRequest): - """ - Upload an SSH private key - --- - tags: - - SSH credentials - parameters: - - in: 'path' - name: 'creds_id' - description: 'ID of the credential to retrieve' - required: true - type: 'integer' - - in: body - name: body - schema: - id: UploadSSHKey - required: - - private_key - - passphrase - properties: - private_key: - type: string - description: The private key string - passphrase: - type: string - description: The passphrase - responses: - 201: - description: SSH key upload successful - """ - try: - self._check_is_correct_group(creds_id) - except Exception as e: - return roxywi_common.handler_exceptions_for_json_data(e, ''), 404 - - try: - ssh_mod.upload_ssh_key(creds_id, body.private_key, body.passphrase) - return BaseResponse().model_dump(mode='json'), 201 - except Exception as e: - return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot upload SSH key') - - @staticmethod - def _check_is_correct_group(creds_id: int): - if g.user_params['role'] == 1: - return True - try: - ssh = cred_sql.get_ssh(creds_id) - except RoxywiResourceNotFound: - raise RoxywiResourceNotFound - if ssh.group_id != g.user_params['group_id']: - raise RoxywiGroupMismatch - - -class CredsView(MethodView): - methods = ['GET'] - decorators = [jwt_required(), get_user_params(), page_for_admin(level=2), check_group()] - - @validate(query=GroupQuery) - def get(self, query: GroupQuery): - """ - Retrieve credential information based on group_id - --- - tags: - - 'SSH credentials' - parameters: - - in: 'query' - name: 'group_id' - description: 'GroupQuery to filter servers. Only for superAdmin role' - required: false - type: 'integer' - responses: - 200: - description: 'Credentials Information' - schema: - type: 'array' - items: - type: 'object' - properties: - group_id: - type: 'integer' - description: 'Group ID the credential belongs to' - id: - type: 'integer' - description: 'Credential ID' - key_enabled: - type: 'integer' - description: 'Key status of the credential' - name: - type: 'string' - description: 'Name of the credential' - username: - type: 'string' - description: 'Username of the credential' - """ - group_id = SupportClass.return_group_id(query) - try: - creds = cred_sql.select_ssh(group=group_id) - json_data = [] - for cred in creds: - json_data.append(model_to_dict(cred, exclude=[Cred.password, Cred.passphrase])) - return jsonify(json_data), 200 - except Exception as e: - return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get credentials') - - class ServerIPView(MethodView): class ServersView(MethodView): methods = ["GET"] diff --git a/requirements.txt b/requirements.txt index 7e07e48..98fcb2e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,6 @@ slack-sdk>=3.4.0 peewee>=3.14.10 PyMySQL>=1.0.2 distro>=1.2.0 -bottle>=0.12.20 psutil>=5.9.1 pdpyras>=4.5.2 pika>=1.3.1