From 1e21290921766005e88b901bac7c9b39cfcba63d Mon Sep 17 00:00:00 2001 From: dingjiahuichina Date: Mon, 21 Jul 2025 11:26:47 +0800 Subject: [PATCH] feat: delete unused web module --- oedp-server/build/service/init_mariadb.sh | 340 ------- oedp-server/build/service/init_service.sh | 33 - .../service/set_ssh_host_key_check_config.sh | 52 -- oedp-server/configs/gunicorn.conf | 9 - oedp-server/configs/mariadb/mariadb.conf | 11 - oedp-server/configs/mariadb/mariadb.json | 1 - oedp-server/configs/silent_installation.conf | 3 - oedp-server/configs/ssh/ssh.conf | 9 - oedp-server/configs/ssh/ssh_prompts.json | 57 -- oedp-server/configs/task.conf | 6 - oedp-server/constants/__init__.py | 13 - oedp-server/constants/auth.py | 20 - oedp-server/constants/configs/__init__.py | 13 - .../constants/configs/account_config.py | 22 - .../constants/configs/mariadb_config.py | 77 -- oedp-server/constants/configs/ssh_conf.py | 88 -- oedp-server/constants/configs/task_config.py | 65 -- oedp-server/constants/paths.py | 39 - oedp-server/manage.py | 34 - oedp-server/plugins/__init__.py | 0 oedp-server/plugins/apps.py | 20 - oedp-server/plugins/migrations/__init__.py | 0 oedp-server/plugins/models.py | 24 - oedp-server/plugins/serializers.py | 36 - oedp-server/plugins/views.py | 44 - oedp-server/resources/__init__.py | 0 oedp-server/resources/local_vars.py | 15 - oedp-server/resources/settings.py | 129 --- oedp-server/resources/urls.py | 31 - oedp-server/resources/wsgi.py | 21 - oedp-server/taskmanager/__init__.py | 0 oedp-server/taskmanager/admin.py | 17 - oedp-server/taskmanager/apps.py | 20 - .../taskmanager/migrations/__init__.py | 0 oedp-server/taskmanager/models.py | 94 -- oedp-server/taskmanager/serializers.py | 270 ------ .../taskmanager/taskscheduler/__init__.py | 0 .../taskmanager/taskscheduler/service.py | 46 - oedp-server/taskmanager/taskscheduler/task.py | 77 -- .../taskscheduler/task_scheduler.py | 406 --------- oedp-server/taskmanager/tests.py | 860 ------------------ oedp-server/taskmanager/views.py | 95 -- oedp-server/usermanager/__init__.py | 0 oedp-server/usermanager/apps.py | 20 - oedp-server/usermanager/jwt_auth/__init__.py | 0 .../usermanager/jwt_auth/authentication.py | 94 -- .../usermanager/jwt_auth/jwt_manager.py | 77 -- oedp-server/usermanager/middlewares.py | 26 - .../usermanager/migrations/__init__.py | 0 oedp-server/usermanager/models.py | 75 -- oedp-server/usermanager/serializers.py | 201 ---- oedp-server/usermanager/views.py | 108 --- oedp-server/utils/__init__.py | 0 oedp-server/utils/cipher.py | 162 ---- oedp-server/utils/cmd_executor.py | 49 - oedp-server/utils/config_parser.py | 47 - oedp-server/utils/file_handler/__init__.py | 13 - .../utils/file_handler/base_handler.py | 60 -- .../utils/file_handler/conf_handler.py | 194 ---- .../utils/file_handler/json_handler.py | 70 -- oedp-server/utils/logger.py | 145 --- oedp-server/utils/pagination.py | 38 - oedp-server/utils/ssh/__init__.py | 13 - oedp-server/utils/ssh/base_connector.py | 23 - oedp-server/utils/ssh/ssh_connector.py | 172 ---- oedp-server/utils/test/__init__.py | 13 - oedp-server/utils/test/testcases.py | 32 - oedp-server/utils/time.py | 43 - oedp-ui/.keep | 0 69 files changed, 4772 deletions(-) delete mode 100644 oedp-server/build/service/init_mariadb.sh delete mode 100644 oedp-server/build/service/init_service.sh delete mode 100644 oedp-server/build/service/set_ssh_host_key_check_config.sh delete mode 100644 oedp-server/configs/gunicorn.conf delete mode 100644 oedp-server/configs/mariadb/mariadb.conf delete mode 100644 oedp-server/configs/mariadb/mariadb.json delete mode 100644 oedp-server/configs/silent_installation.conf delete mode 100644 oedp-server/configs/ssh/ssh.conf delete mode 100644 oedp-server/configs/ssh/ssh_prompts.json delete mode 100644 oedp-server/configs/task.conf delete mode 100644 oedp-server/constants/__init__.py delete mode 100644 oedp-server/constants/auth.py delete mode 100644 oedp-server/constants/configs/__init__.py delete mode 100644 oedp-server/constants/configs/account_config.py delete mode 100644 oedp-server/constants/configs/mariadb_config.py delete mode 100644 oedp-server/constants/configs/ssh_conf.py delete mode 100644 oedp-server/constants/configs/task_config.py delete mode 100644 oedp-server/constants/paths.py delete mode 100644 oedp-server/manage.py delete mode 100644 oedp-server/plugins/__init__.py delete mode 100644 oedp-server/plugins/apps.py delete mode 100644 oedp-server/plugins/migrations/__init__.py delete mode 100644 oedp-server/plugins/models.py delete mode 100644 oedp-server/plugins/serializers.py delete mode 100644 oedp-server/plugins/views.py delete mode 100644 oedp-server/resources/__init__.py delete mode 100644 oedp-server/resources/local_vars.py delete mode 100644 oedp-server/resources/settings.py delete mode 100644 oedp-server/resources/urls.py delete mode 100644 oedp-server/resources/wsgi.py delete mode 100644 oedp-server/taskmanager/__init__.py delete mode 100644 oedp-server/taskmanager/admin.py delete mode 100644 oedp-server/taskmanager/apps.py delete mode 100644 oedp-server/taskmanager/migrations/__init__.py delete mode 100644 oedp-server/taskmanager/models.py delete mode 100644 oedp-server/taskmanager/serializers.py delete mode 100644 oedp-server/taskmanager/taskscheduler/__init__.py delete mode 100644 oedp-server/taskmanager/taskscheduler/service.py delete mode 100644 oedp-server/taskmanager/taskscheduler/task.py delete mode 100644 oedp-server/taskmanager/taskscheduler/task_scheduler.py delete mode 100644 oedp-server/taskmanager/tests.py delete mode 100644 oedp-server/taskmanager/views.py delete mode 100644 oedp-server/usermanager/__init__.py delete mode 100644 oedp-server/usermanager/apps.py delete mode 100644 oedp-server/usermanager/jwt_auth/__init__.py delete mode 100644 oedp-server/usermanager/jwt_auth/authentication.py delete mode 100644 oedp-server/usermanager/jwt_auth/jwt_manager.py delete mode 100644 oedp-server/usermanager/middlewares.py delete mode 100644 oedp-server/usermanager/migrations/__init__.py delete mode 100644 oedp-server/usermanager/models.py delete mode 100644 oedp-server/usermanager/serializers.py delete mode 100644 oedp-server/usermanager/views.py delete mode 100644 oedp-server/utils/__init__.py delete mode 100644 oedp-server/utils/cipher.py delete mode 100644 oedp-server/utils/cmd_executor.py delete mode 100644 oedp-server/utils/config_parser.py delete mode 100644 oedp-server/utils/file_handler/__init__.py delete mode 100644 oedp-server/utils/file_handler/base_handler.py delete mode 100644 oedp-server/utils/file_handler/conf_handler.py delete mode 100644 oedp-server/utils/file_handler/json_handler.py delete mode 100644 oedp-server/utils/logger.py delete mode 100644 oedp-server/utils/pagination.py delete mode 100644 oedp-server/utils/ssh/__init__.py delete mode 100644 oedp-server/utils/ssh/base_connector.py delete mode 100644 oedp-server/utils/ssh/ssh_connector.py delete mode 100644 oedp-server/utils/test/__init__.py delete mode 100644 oedp-server/utils/test/testcases.py delete mode 100644 oedp-server/utils/time.py delete mode 100644 oedp-ui/.keep diff --git a/oedp-server/build/service/init_mariadb.sh b/oedp-server/build/service/init_mariadb.sh deleted file mode 100644 index 1139261..0000000 --- a/oedp-server/build/service/init_mariadb.sh +++ /dev/null @@ -1,340 +0,0 @@ -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-15 -# ====================================================================================================================== - -is_silent_installation=$1 -CONFIG_FILE="/etc/oedp-server/silent_installation.conf" - -# 读取配置文件 -function read_config_value { - local key="$1" - - # 判断配置文件中 key 是否存在 - local item=$(grep "^${key} =" "$config_file") - if [[ -z "${item}" ]]; then - echo -e "\e[1;31mThe key '${key}' not found in the configuration file ${CONFIG_FILE}\e[0m" - exit 1 - fi - - # 判断配置文件中对应 key 的值是否存在 - local value=$(echo "${item}" | awk -F= '{print $2}' | xargs) - if [[ -n "${value}" ]]; then - echo "${value}" - else - echo -e "\e[1;31mThe value of key '${key}' is empty in the configuration file ${CONFIG_FILE}\e[0m" - exit 1 - fi -} - -# 检查密码复杂度,密码至少包含8个字符,大小写字母、数字、特殊符号三种以上 -function check_password_complexity() -{ - local variable_content=$1 - local complexity=0 - - if [[ ${#variable_content} -ge 8 ]]; then - complexity=$((${complexity}+1)) - else - return 1 - fi - - if [[ "${variable_content}" =~ [[:upper:]] ]]; then - complexity=$((${complexity}+1)) - fi - - if [[ "${variable_content}" =~ [[:lower:]] ]]; then - complexity=$((${complexity}+1)) - fi - - if [[ "${variable_content}" =~ [[:digit:]] ]]; then - complexity=$((${complexity}+1)) - fi - - if [[ "${variable_content}" =~ [[:punct:]] ]]; then - complexity=$((${complexity}+1)) - fi - - unset variable_content - if [[ "${complexity}" -ge 4 ]]; then - return 0 - else - return 1 - fi -} - - -# 检查自定义数据库名字复杂度,自定义数据库名字至少包含2个字符,大小写字母、下划线、数字两种以上 -function check_database_name() -{ - local variable_content=$1 - local complexity=0 - - if [[ "${variable_content}" =~ [[:upper:]] ]]; then - complexity=$((${complexity}+1)) - fi - - if [[ "${variable_content}" =~ [[:lower:]] ]]; then - complexity=$((${complexity}+1)) - fi - - if [[ "${variable_content}" =~ [[:digit:]] ]]; then - complexity=$((${complexity}+1)) - fi - - if [[ "${variable_content}" =~ "_" ]]; then - complexity=$((${complexity}+1)) - fi - # 包含 a-zA-Z0-9_ 之外的字符都不符合要求 - if [[ "${variable_content}" =~ [^a-zA-Z0-9_] ]]; then - complexity=0 - fi - - unset variable_content - if [[ ${complexity} -ge 2 ]]; then - return 0 - else - return 1 - fi -} - - -echo -e "\e[1;34mStart to configure MariaDB for oeDploy WebServer.\e[0m" -while true -do - # 交互式安装会询问 MariaDB 是否已配置,静默安装默认没有被配置 - if [ "${is_silent_installation}" == "false" ]; then - read -p "Whether MariaDB is configured? [Y/n] (default: n) " Y_N - else - Y_N="n" - fi - # 判断 MariaDB 是否已配置,若已配置则跳过,若没有则开始配置 - if [[ "${Y_N}" == "y" || "${Y_N}" == "Y" ]]; then - break - elif [[ ! -n "${Y_N}" || "${Y_N}" == "N" || "${Y_N}" == "n" ]]; then - # 启动 MariaDB 服务 - systemctl start mariadb - if [[ $? -ne 0 ]]; then - echo -e "\e[1;31mThe MariaDB service fails to start.\e[0m" - exit 1 - fi - # 检查服务是否启动 - mariadb_status=$(systemctl is-active mariadb) - if [[ "${mariadb_status}" == "active" ]]; then - echo -e "\e[1;32mThe MariaDB service is active.\e[0m" - else - echo -e "\e[1;31mThe MariaDB service is inactive.\e[0m" - exit 1 - fi - # 设置为开机自启动服务 - systemctl enable mariadb - - # 初始化数据库 - if [ "${is_silent_installation}" == "false" ]; then - mysql_secure_installation - else - default_root_pw="\n" - set_root_pw="y" - root_password=$(read_config_value "root_password") - change_root_pw="n" - remove_anonymous_users="y" - disallow_root_login_remotely="n" - remove_test_database_and_access_to_it="y" - reload_privilege_tables="y" - # 首次安装,mariadb 的 root 密码为空,需要设置 root 密码 - input_string="${default_root_pw}" - input_string+="${set_root_pw}\n" - input_string+="${root_password}\n" - input_string+="${root_password}\n" - input_string+="${remove_anonymous_users}\n" - input_string+="${disallow_root_login_remotely}\n" - input_string+="${remove_test_database_and_access_to_it}\n" - input_string+="${reload_privilege_tables}\n" - expect -c " -set timeout 5 -spawn mysql_secure_installation -expect \"Enter current password for root (enter for none):\" -send \"${input_string}\" -expect { - \"*Access denied*\" { - exit 1 - } - eof -} -" - return_code=$? - if [[ "${return_code}" == "1" ]]; then - # 非首次安装,mariadb 的 root 密码已设置 - input_string="${root_password}\n" - input_string+="${change_root_pw}\n" - input_string+="${remove_anonymous_users}\n" - input_string+="${disallow_root_login_remotely}\n" - input_string+="${remove_test_database_and_access_to_it}\n" - input_string+="${reload_privilege_tables}\n" - expect -c " -set timeout 5 -spawn mysql_secure_installation -expect \"Enter current password for root (enter for none):\" -send \"${input_string}\" -expect { - \"*Access denied*\" { - puts \"Error: Incorrect mariadb root password. Please change the value of root_password in the ${CONFIG_FILE}\" - exit 1 - } - eof -} -" - if [[ "$?" == "1" ]]; then - exit 1 - fi - fi - fi - echo "" - break - else - echo -e "\e[1;31mThe input is invalid. Please input again.\e[0m" - fi -done - -unset Y_N - -# 检查防火墙是否启动,如果启动则检查 3306 端口是否在防火墙白名单中,如果不存在则添加到白名单中 -if systemctl is-active --quiet firewalld; then - port_3306=$(firewall-cmd --query-port=3306/tcp) - if [[ "${port_3306}" == "no" ]]; then - port_3306=$(firewall-cmd --zone=public --add-port=3306/tcp --permanent) - firewall-cmd --reload - fi - port_3306=$(firewall-cmd --query-port=3306/tcp) - if [[ "${port_3306}" != "yes" ]]; then - echo -e "\e[1;31mFailed to enable port 3306.\e[0m" - exit 1 - fi -fi - -# 输入或获取 oedp 密码 -stty -echo -if [ "${is_silent_installation}" == "false" ]; then - should_break=false - for i in {1..5}; do - read -p "Enter the password of oedp user for MariaDB: " oedp_passwd_01 - echo "" - read -p "Confirm: " oedp_passwd_02 - if [[ "${oedp_passwd_01}" == "${oedp_passwd_02}" ]]; then - should_break=true - break - fi - echo -e "\e[1;33mThe provided passwords do not match. Please re-enter them for verification. \e[0m" - done - if [ ! ${should_break} ]; then - exit 1 - fi - oedp_passwd=${oedp_passwd_01} -else - oedp_passwd=$(read_config_value "oedp_password") -fi -stty echo -echo "" - -# 检查 oedp 用户密码的复杂度 -while true -do - check_password_complexity ${oedp_passwd} - if [[ $? -ne 0 ]]; then - stty -echo - if [ "${is_silent_installation}" == "false" ]; then - echo -e "\e[1;34mThe password must contain at least eight characters, including uppercase lowercase digits and special characters.\e[0m" - echo -e "\e[1;31mThe password of the oedp user for MariaDB is invalid. Please input again.\e[0m" - should_break=false - for i in {1..5}; do - read -p "Enter the password of oedp user for MariaDB: " oedp_passwd_01 - echo "" - read -p "Confirm: " oedp_passwd_02 - if [[ "${oedp_passwd_01}" == "${oedp_passwd_02}" ]]; then - should_break=true - break - fi - echo -e "\e[1;33mThe provided passwords do not match. Please re-enter them for verification. \e[0m" - done - if [ ! ${should_break} ]; then - exit 1 - fi - oedp_passwd=${oedp_passwd_01} - else - echo -e "\e[1;34mThe password must contain at least eight characters, including uppercase lowercase digits and special characters.\e[0m" - echo -e "\e[1;31mThe password of the oedp user for MariaDB is invalid. Please change the value of oedp_password in the ${CONFIG_FILE}.\e[0m" - stty echo - exit 1 - fi - stty echo - echo "" - else - break - fi -done - -# 获取自定义数据库名 -while true -do - echo -e "\e[1;34mIf the selected database already exists, it will be overwritten.\e[0m" - if [ "${is_silent_installation}" == "false" ]; then - read -p "Use default oedp_db database? [Y/n] (default: Y) " Y_N - else - Y_N="Y" - fi - # 使用默认 - if [[ ! -n "${Y_N}" || "${Y_N}" == "y" || "${Y_N}" == "Y" ]]; then - mariadb_name=oedp_db - break - elif [[ "${Y_N}" == "N" || "${Y_N}" == "n" ]]; then - # 用户自定义数据库 - read -p "Please input the name of the database to be created: " mariadb_name - check_database_name ${mariadb_name} - if [[ $? -ne 0 ]]; then - echo -e "\e[1;34mThe database name must contain at least two types of characters, including uppercase lowercase underscores and digits.\e[0m" - echo -e "\e[1;31mThe input database name entered is invalid. Please input again.\e[0m" - else - break - fi - else - echo -e "\e[1;31mThe input is invalid. Please input again.\e[0m" - fi -done -unset Y_N - -# 创建用户名为 oedp 的数据库 -stty -echo -if [ "${is_silent_installation}" == "false" ]; then - read -p "Enter the password of the root user of the MariaDB again: " root_password -else - root_password=$(read_config_value "root_password") -fi -stty echo -mysql -uroot -p${root_password} << EOF -DROP DATABASE IF EXISTS ${mariadb_name}; -CREATE DATABASE IF NOT EXISTS ${mariadb_name} CHARACTER SET utf8 COLLATE utf8_bin; - -DELETE FROM mysql.user WHERE User='oedp'; -DELETE FROM mysql.db WHERE User='oedp'; -flush privileges; -# oedp 用户权限仅限操作自定义新创建的数据库 -CREATE USER 'oedp'@'localhost' IDENTIFIED BY '${mariadb_passwd}'; -GRANT ALL ON ${mariadb_name}.* TO 'oedp'@'localhost' IDENTIFIED BY '${mariadb_passwd}' WITH GRANT OPTION; -flush privileges; -EOF - -unset root_password - -if [[ $? -ne 0 ]]; then - echo -e "\e[1;31mMariaDB configurations are incorrect. Please check the MariaDB root password or the status of MariaDB.\e[0m" - exit 1 -fi - -echo -e "\e[1;32mMariaDB is configured successfully.\e[0m" \ No newline at end of file diff --git a/oedp-server/build/service/init_service.sh b/oedp-server/build/service/init_service.sh deleted file mode 100644 index 9c72110..0000000 --- a/oedp-server/build/service/init_service.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-15 -# ====================================================================================================================== - - -# 判断是否静默安装 -if [ -z "$1" ]; then - is_silent_installation="false" -else - if [[ "$1" == "-s" ]]; then - is_silent_installation="true" - else - echo -e "\e[1;31mParameter [$1] is invalid, please use the '-s' parameter to indicate silent installation.\e[0m" - fi -fi - -SERVICE_DIR=$(realpath $(dirname $0)) - - -# 配置 mariadb -source "${SERVICE_DIR}"/init_service.sh ${is_silent_installation} - -# 根据用户输入修改ssh连接是否需要匹配公钥 -source "${SERVICE_DIR}"/set_ssh_host_key_check_config.sh ${is_silent_installation} diff --git a/oedp-server/build/service/set_ssh_host_key_check_config.sh b/oedp-server/build/service/set_ssh_host_key_check_config.sh deleted file mode 100644 index 1d92c39..0000000 --- a/oedp-server/build/service/set_ssh_host_key_check_config.sh +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-21 -# ====================================================================================================================== - -# 根据用户输入修改ssh连接是否需要匹配公钥 - -is_silent_installation=$1 -SSH_CONFIG_FILE="/etc/ssh/ssh_config" - -# 获取用户输入 - while true - do - echo -e "\e[1;40;34mIf authentication is enabled,\e[0m" - echo -e "\e[1;40;34mthe SSH connection fails after the fingerprint of the machine changes.\e[0m" - echo -n -e "\e[1;40;34mPlease confirm whether public key authentication is not required for SSH connection(y/n default: n):\e[0m" - if [ "${is_silent_installation}" == "false" ]; then - read -r answer - else - answer="n" - fi - if [ -z "${answer}" ] - then - answer="n" - break; - elif [ "${answer}" == "n" ] || [ "${answer}" == "y" ] - then - break; - else - echo -e "\e[1;31mPlease input 'y' or 'n'\e[0m" - fi - done - # 根据用户输入对ssh配置文件做修改 - if [ ! -w "${SSH_CONFIG_FILE}" ];then - echo -e "\e[1;31m${SSH_CONFIG_FILE} can not write, modify fail.\e[0m" - exit 1 - fi - if [ "${answer}" == "y" ];then - sed -i 's/^StrictHostKeyChecking.*//' ${SSH_CONFIG_FILE} - sed -i 's/^\s*StrictHostKeyChecking.*//' ${SSH_CONFIG_FILE} - sed -i 's/^UserKnownHostsFile.*//' ${SSH_CONFIG_FILE} - sed -i 's/^\s*UserKnownHostsFile.*//' ${SSH_CONFIG_FILE} - sed -i '$a StrictHostKeyChecking no' ${SSH_CONFIG_FILE} - sed -i '$a UserKnownHostsFile=\/dev\/null' ${SSH_CONFIG_FILE} - fi diff --git a/oedp-server/configs/gunicorn.conf b/oedp-server/configs/gunicorn.conf deleted file mode 100644 index f36bbcb..0000000 --- a/oedp-server/configs/gunicorn.conf +++ /dev/null @@ -1,9 +0,0 @@ -# 绑定ip和端口号 -bind = '127.0.0.1:18080' -# 监听队列 -backlog = 512 -# 超时 -timeout = 600 -# 进程数 -workers = 1 -threads = 8 \ No newline at end of file diff --git a/oedp-server/configs/mariadb/mariadb.conf b/oedp-server/configs/mariadb/mariadb.conf deleted file mode 100644 index 6b810ea..0000000 --- a/oedp-server/configs/mariadb/mariadb.conf +++ /dev/null @@ -1,11 +0,0 @@ -[mariadb] -# 数据库名称 -name = oedp_db -# 数据库地址 -host = 127.0.0.1 -# 端口 -port = 3306 -# 数据库用户名 -user = oedp -# 数据库密码 -password = \ No newline at end of file diff --git a/oedp-server/configs/mariadb/mariadb.json b/oedp-server/configs/mariadb/mariadb.json deleted file mode 100644 index 6ab1531..0000000 --- a/oedp-server/configs/mariadb/mariadb.json +++ /dev/null @@ -1 +0,0 @@ -{"work_key_encrypted": "", "work_key_encrypted_iv": "", "encrypted_iv": "", "half_key1": ""} \ No newline at end of file diff --git a/oedp-server/configs/silent_installation.conf b/oedp-server/configs/silent_installation.conf deleted file mode 100644 index e7386bb..0000000 --- a/oedp-server/configs/silent_installation.conf +++ /dev/null @@ -1,3 +0,0 @@ -root_password = -# oedp 密码至少包含8个字符,大小写字母、数字、特殊符号三种以上 -oedp_password = \ No newline at end of file diff --git a/oedp-server/configs/ssh/ssh.conf b/oedp-server/configs/ssh/ssh.conf deleted file mode 100644 index 5823209..0000000 --- a/oedp-server/configs/ssh/ssh.conf +++ /dev/null @@ -1,9 +0,0 @@ -[timeout] -establish_timeout = 120 -expect_prompts_timeout = 120 -execute_cmd_timeout = 600 - -[size] -tail_show_line_num = 100 -window_height = 24 -window_buffer_width = 150 \ No newline at end of file diff --git a/oedp-server/configs/ssh/ssh_prompts.json b/oedp-server/configs/ssh/ssh_prompts.json deleted file mode 100644 index 4185761..0000000 --- a/oedp-server/configs/ssh/ssh_prompts.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "login": [ - { - "prompt": "[p/P]assword:", - "send": "PASSWORD", - "status": "In Progress", - "next": null - }, - { - "prompt": "Are you sure you want to continue connecting", - "send": "yes\n", - "status": "In Progress", - "next": { - "prompt": "[p/P]assword:", - "send": "PASSWORD", - "status": "In Progress", - "next": null - } - }, - { - "prompt": "Last login:", - "send": null, - "status": "Success", - "next": null - }, - { - "prompt": "上一次登录:", - "send": null, - "status": "Success", - "next": null - }, - { - "prompt": "[p/P]ermission denied", - "send": null, - "status": "Fail", - "next": null - }, - { - "prompt": "ssh: connect to host", - "send": null, - "status": "Fail", - "next": null - }, - { - "prompt": "Connection refused", - "send": null, - "status": "Fail", - "next": null - }, - { - "prompt": "Host key verification failed", - "send": null, - "status": "Fail", - "next": null - } - ] -} \ No newline at end of file diff --git a/oedp-server/configs/task.conf b/oedp-server/configs/task.conf deleted file mode 100644 index c5ffc08..0000000 --- a/oedp-server/configs/task.conf +++ /dev/null @@ -1,6 +0,0 @@ -[scheduler] -max_task_number = 1000 -max_repo_number = 50 -max_user_task_number = 50 -max_task_node = 1000 -thread_timeout = 30 \ No newline at end of file diff --git a/oedp-server/constants/__init__.py b/oedp-server/constants/__init__.py deleted file mode 100644 index 6dbd16b..0000000 --- a/oedp-server/constants/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-22 -# ====================================================================================================================== diff --git a/oedp-server/constants/auth.py b/oedp-server/constants/auth.py deleted file mode 100644 index fb51600..0000000 --- a/oedp-server/constants/auth.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-05 -# ====================================================================================================================== - -from django.conf import settings - -JWT_SECRET_KEY = settings.SECRET_KEY -JWT_ALGORITHM = 'HS256' -JWT_EXPIRY_DAYS = 1 -JWT_AUTH_HEADER_PREFIX = 'JWT' diff --git a/oedp-server/constants/configs/__init__.py b/oedp-server/constants/configs/__init__.py deleted file mode 100644 index 6dbd16b..0000000 --- a/oedp-server/constants/configs/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-22 -# ====================================================================================================================== diff --git a/oedp-server/constants/configs/account_config.py b/oedp-server/constants/configs/account_config.py deleted file mode 100644 index 3453d4c..0000000 --- a/oedp-server/constants/configs/account_config.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-21 -# ====================================================================================================================== - -# 管理员用户在数据库中的 ID -ADMIN_ID = 1 -# 盐值长度 -SALT_LENGTH = 18 -# 用户名最大长度 -USERNAME_MAX_LEN = 32 -# 用户名最小长度 -USERNAME_MIN_LEN = 6 \ No newline at end of file diff --git a/oedp-server/constants/configs/mariadb_config.py b/oedp-server/constants/configs/mariadb_config.py deleted file mode 100644 index f89bb3c..0000000 --- a/oedp-server/constants/configs/mariadb_config.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-16 -# ====================================================================================================================== -import configparser -import json - -from constants.paths import MARIADB_CONFIG_FILE, MARIADB_JSON_FILE -from utils.cipher import OEDPCipher -from utils.file_handler.base_handler import FileError -from utils.file_handler.conf_handler import ConfHandler - -__all__ = ['MariaDBConfig', 'get_settings_mariadb_config'] - -DEFAULT_NAME = 'oedp_db' -DEFAULT_HOST = '127.0.0.1' -DEFAULT_PORT = 3306 -DEFAULT_USER = 'oedp' -DEFAULT_PASSWORD = '' - - -class MariaDBConfig: - NAME = DEFAULT_NAME - HOST = DEFAULT_HOST - PORT = DEFAULT_PORT - USER = DEFAULT_USER - PASSWORD = DEFAULT_PASSWORD - - -try: - conf_handler = ConfHandler(file_path=MARIADB_CONFIG_FILE) -except (FileError, configparser.MissingSectionHeaderError, configparser.ParsingError): - pass -else: - MariaDBConfig.NAME = conf_handler.get('mariadb', 'name', default=DEFAULT_NAME) - MariaDBConfig.HOST = conf_handler.get('mariadb', 'host', default=DEFAULT_HOST) - try: - MariaDBConfig.PORT = conf_handler.getint('mariadb', 'port', default=DEFAULT_PORT) - except ValueError: - pass - MariaDBConfig.USER = conf_handler.get('mariadb', 'user', default=DEFAULT_USER) - MariaDBConfig.PASSWORD = conf_handler.get('mariadb', 'password', default=DEFAULT_PASSWORD) - - -def get_settings_mariadb_config(): - with open(MARIADB_JSON_FILE, mode='r') as fr_handle: - ciphertext_data = json.load(fr_handle) - oedp_cipher = OEDPCipher() - plaintext = oedp_cipher.decrypt_ciphertext_data(ciphertext_data) - database_config = { - 'NAME': MariaDBConfig.NAME, - 'HOST': MariaDBConfig.HOST, - 'PORT': MariaDBConfig.PORT, - 'USER': MariaDBConfig.USER, - 'PASSWORD': plaintext, - 'ENGINE': 'django.db.backends.mysql', - 'OPTIONS': { - 'init_command': 'SET sql_mode="STRICT_TRANS_TABLES"', - 'charset': 'utf8', - 'autocommit': True - }, - 'TEST': { - 'CHARSET': 'utf8', - 'COLLATION': 'utf8_bin' - } - } - del plaintext - return database_config diff --git a/oedp-server/constants/configs/ssh_conf.py b/oedp-server/constants/configs/ssh_conf.py deleted file mode 100644 index 9e678f4..0000000 --- a/oedp-server/constants/configs/ssh_conf.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-13 -# ====================================================================================================================== - -from configparser import MissingSectionHeaderError, ParsingError - -from constants.paths import SSH_CONFIG_FILE -from utils.file_handler.base_handler import FileError -from utils.file_handler.conf_handler import ConfHandler -from utils.logger import init_log - -__all__ = ['SSHConfig'] - -run_logger = init_log("run.log") - -# 默认 SSH 建立连接的超时时间 -DEFAULT_ESTABLISH_TIMEOUT = 120 -# 默认命令提示符匹配超时时间 -DEFAULT_EXPECT_PROMPTS_TIMEOUT = 120 -# 默认 SSH 执行命令超时时间 -DEFAULT_EXECUTE_CMD_TIMEOUT = 600 -# 默认命令输出打印行数 -DEFAULT_TAIL_SHOW_LINE_NUM = 100 -# 默认命令行窗口高度 -DEFAULT_WINDOW_HEIGHT = 24 -# 默认命令行窗口余量宽度 -DEFAULT_WINDOW_BUFFER_WIDTH = 150 - - -class SSHConfig: - # SSH 建立连接的超时时间 - ESTABLISH_TIMEOUT = DEFAULT_ESTABLISH_TIMEOUT - # 命令提示符匹配超时时间 - EXPECT_PROMPTS_TIMEOUT = DEFAULT_EXPECT_PROMPTS_TIMEOUT - # SSH 执行命令超时时间 - EXECUTE_CMD_TIMEOUT = DEFAULT_EXECUTE_CMD_TIMEOUT - # tail 命令输出打印行数 - TAIL_SHOW_LINE_NUM = DEFAULT_TAIL_SHOW_LINE_NUM - # 命令行窗口高度 - WINDOW_HEIGHT = DEFAULT_WINDOW_HEIGHT - # 命令行窗口余量宽度,该宽度加上命令长度组成命令行窗口宽度 - WINDOW_BUFFER_WIDTH = DEFAULT_WINDOW_BUFFER_WIDTH - - -try: - conf_handler = ConfHandler(file_path=SSH_CONFIG_FILE, logger=run_logger) -except (FileError, MissingSectionHeaderError, ParsingError): - pass -else: - try: - SSHConfig.ESTABLISH_TIMEOUT = conf_handler.getint( - 'timeout', 'establish_timeout', default=DEFAULT_ESTABLISH_TIMEOUT) - except ValueError: - pass - try: - SSHConfig.EXPECT_PROMPTS_TIMEOUT = conf_handler.getint( - 'timeout', 'expect_prompts_timeout', default=DEFAULT_EXPECT_PROMPTS_TIMEOUT) - except ValueError: - pass - try: - SSHConfig.EXECUTE_CMD_TIMEOUT = conf_handler.getint( - 'timeout', 'execute_cmd_timeout', default=DEFAULT_EXECUTE_CMD_TIMEOUT) - except ValueError: - pass - try: - SSHConfig.TAIL_SHOW_LINE_NUM = conf_handler.getint( - 'size', 'tail_show_line_num', default=DEFAULT_TAIL_SHOW_LINE_NUM) - except ValueError: - pass - try: - SSHConfig.WINDOW_HEIGHT = conf_handler.getint('size', 'window_height', default=DEFAULT_WINDOW_HEIGHT) - except ValueError: - pass - try: - SSHConfig.WINDOW_BUFFER_WIDTH = conf_handler.getint( - 'size', 'window_buffer_width', default=DEFAULT_WINDOW_BUFFER_WIDTH) - except ValueError: - pass diff --git a/oedp-server/constants/configs/task_config.py b/oedp-server/constants/configs/task_config.py deleted file mode 100644 index bf74c0c..0000000 --- a/oedp-server/constants/configs/task_config.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-15 -# ====================================================================================================================== - -from configparser import MissingSectionHeaderError, ParsingError - -from constants.paths import TASK_CONFIG_FILE -from utils.file_handler.base_handler import FileError -from utils.file_handler.conf_handler import ConfHandler -from utils.logger import init_log - -__all__ = ['TaskConfig'] - -run_logger = init_log("run.log") - -DEFAULT_MAX_TASK_NUMBER = 1000 -DEFAULT_MAX_REPO_NUMBER = 50 -DEFAULT_MAX_USER_TASK_NUMBER = 50 -DEFAULT_MAX_TASK_NODE = 1000 -DEFAULT_THREAD_TIMEOUT = 30 - - -class TaskConfig: - MAX_TASK_NUMBER = DEFAULT_MAX_TASK_NUMBER - MAX_REPO_NUMBER = DEFAULT_MAX_REPO_NUMBER - MAX_USER_TASK_NUMBER = DEFAULT_MAX_USER_TASK_NUMBER - MAX_TASK_NODE = DEFAULT_MAX_TASK_NODE - THREAD_TIMEOUT = DEFAULT_THREAD_TIMEOUT - - -try: - conf_handler = ConfHandler(file_path=TASK_CONFIG_FILE, logger=run_logger) -except (FileError, MissingSectionHeaderError, ParsingError): - pass -else: - try: - TaskConfig.MAX_TASK_NUMBER = conf_handler.getint( - 'scheduler', 'max_task_number', default=DEFAULT_MAX_TASK_NUMBER) - except ValueError: - pass - try: - TaskConfig.MAX_REPO_NUMBER = conf_handler.getint( - 'scheduler', 'max_repo_number', default=DEFAULT_MAX_REPO_NUMBER) - except ValueError: - pass - TaskConfig.MAX_USER_TASK_NUMBER = conf_handler.getint( - 'scheduler', 'max_user_task_number', default=DEFAULT_MAX_USER_TASK_NUMBER) - try: - TaskConfig.MAX_TASK_NODE = conf_handler.getint('scheduler', 'max_task_node', default=DEFAULT_MAX_TASK_NODE) - except ValueError: - pass - try: - TaskConfig.THREAD_TIMEOUT = conf_handler.getint('scheduler', 'thread_timeout', default=DEFAULT_THREAD_TIMEOUT) - except ValueError: - pass diff --git a/oedp-server/constants/paths.py b/oedp-server/constants/paths.py deleted file mode 100644 index fe78294..0000000 --- a/oedp-server/constants/paths.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -import os - -# oedp-server 工作目录 -OEDP_SERVER_WORK_DIR = '/var/oedp-server' -# oedp-server 日志目录 -LOG_DIR = os.path.join(OEDP_SERVER_WORK_DIR, 'log') -# 第三方软件目录 -THIRDAPP_INSTALL_DIR = os.path.join(OEDP_SERVER_WORK_DIR, 'thirdapps') -# OpenSSL 目录 -OPENSSL_DIR = os.path.join(THIRDAPP_INSTALL_DIR, 'openssl') -# libcrypto.so 文件路径 -LIBCRYPTO_SO_FILE = os.path.join(OPENSSL_DIR, 'libcrypto.so') - -# oedp-server 配置文件路径 -OEDP_SERVER_CONFIG_DIR = '/etc/oedp-server' -# task.conf 配置文件路径 -TASK_CONFIG_FILE = os.path.join(OEDP_SERVER_CONFIG_DIR, 'task.conf') -# ssh.conf 配置文件路径 -SSH_CONFIG_FILE = os.path.join(OEDP_SERVER_CONFIG_DIR, 'ssh', 'ssh.conf') -# SSH 提示符配置文件 -SSH_PROMPTS_JSON_FILE = os.path.join(OEDP_SERVER_CONFIG_DIR, 'ssh', 'ssh_prompts.json') -# mariadb.conf 配置文件路径 -MARIADB_CONFIG_FILE = os.path.join(OEDP_SERVER_CONFIG_DIR, 'mariadb', 'mariadb.conf') -# MariaDB 密文数据 json 文件 -MARIADB_JSON_FILE = os.path.join(OEDP_SERVER_CONFIG_DIR, 'mariadb', 'mariadb.json') diff --git a/oedp-server/manage.py b/oedp-server/manage.py deleted file mode 100644 index 1ca6e8f..0000000 --- a/oedp-server/manage.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'resources.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() diff --git a/oedp-server/plugins/__init__.py b/oedp-server/plugins/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/oedp-server/plugins/apps.py b/oedp-server/plugins/apps.py deleted file mode 100644 index 7afde01..0000000 --- a/oedp-server/plugins/apps.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-07 -# ====================================================================================================================== - -from django.apps import AppConfig - - -class PluginsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'plugins' diff --git a/oedp-server/plugins/migrations/__init__.py b/oedp-server/plugins/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/oedp-server/plugins/models.py b/oedp-server/plugins/models.py deleted file mode 100644 index ef430a0..0000000 --- a/oedp-server/plugins/models.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-07 -# ====================================================================================================================== - -from django.db import models - - -class Plugin(models.Model): - name = models.CharField('插件名称', max_length=32) - version = models.CharField('插件版本', max_length=32, blank=True, null=True) - description = models.TextField('插件描述', max_length=10000, blank=True, null=True) - icon_path = models.CharField('icon 文件存储地址', max_length=255, blank=True, null=True) - created_at = models.DateTimeField('创建时间', auto_now_add=True) - updated_at = models.DateTimeField('更新时间', auto_now=True) diff --git a/oedp-server/plugins/serializers.py b/oedp-server/plugins/serializers.py deleted file mode 100644 index b31c342..0000000 --- a/oedp-server/plugins/serializers.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-07 -# ====================================================================================================================== - -from rest_framework import serializers - -from plugins.models import Plugin - - -class PluginSerializer(serializers.ModelSerializer): - - class Meta: - model = Plugin - fields = ( - 'id', - 'name', - 'version', - 'description', - ) - - def create(self, validated_data): - return Plugin.objects.create(**validated_data) - - -class PluginIDSerializer(serializers.Serializer): - id = serializers.IntegerField() diff --git a/oedp-server/plugins/views.py b/oedp-server/plugins/views.py deleted file mode 100644 index 37c0c0c..0000000 --- a/oedp-server/plugins/views.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-07 -# ====================================================================================================================== - -from rest_framework import viewsets -from rest_framework.response import Response -from rest_framework import status - -from plugins.models import Plugin -from plugins.serializers import PluginSerializer - - -class PluginViewSet(viewsets.GenericViewSet): - queryset = Plugin.objects.all() - - def list(self, request): - plugins = self.paginate_queryset(self.get_queryset()) - serializer = PluginSerializer(plugins, many=True) - return self.get_paginated_response(serializer.data) - - def create(self, request): - serializer = PluginSerializer(data=request.data) - if not serializer.is_valid(): - return Response({ - 'is_success': False, - 'message': "Failed to add a plugin.", - 'errors': serializer.errors - }, status=status.HTTP_400_BAD_REQUEST) - serializer.save() - return Response({ - 'is_success': True, - 'message': 'Add a plugin successfully.', - 'data': serializer.data - }) diff --git a/oedp-server/resources/__init__.py b/oedp-server/resources/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/oedp-server/resources/local_vars.py b/oedp-server/resources/local_vars.py deleted file mode 100644 index d9bb62c..0000000 --- a/oedp-server/resources/local_vars.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -RESOURCE_STRING = 'd7g#cj7lsc9b7ki9h_!j2*fig+2u3c*%14(-9p46%(yjnhjfq(' diff --git a/oedp-server/resources/settings.py b/oedp-server/resources/settings.py deleted file mode 100644 index 0f9464b..0000000 --- a/oedp-server/resources/settings.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -from pathlib import Path - -from resources.local_vars import RESOURCE_STRING - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = RESOURCE_STRING - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False -APPEND_SLASH = True - -ALLOWED_HOSTS = ['*'] - -# Setting the system user model -AUTH_USER_MODEL = 'usermanager.User' - -# Application definition -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'taskmanager.apps.TaskmanagerConfig', - 'usermanager.apps.UsermanagerConfig', - 'plugins.apps.PluginsConfig', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'resources.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'resources.wsgi.application' - -# Database -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } -} - -# Password validation -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - -# Internationalization -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - -# Static files (CSS, JavaScript, Images) -STATIC_URL = '/static/' - -# Default primary key field type -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -# Rest Framework -REST_FRAMEWORK = { - # 分页 - 'DEFAULT_PAGINATION_CLASS': 'utils.pagination.CustomPageNumberPagination', - # 身份认证 - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'usermanager.jwt_auth.authentication.TokenAuthentication', - ), - # 权限认证 - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', - ), -} diff --git a/oedp-server/resources/urls.py b/oedp-server/resources/urls.py deleted file mode 100644 index b64d9e0..0000000 --- a/oedp-server/resources/urls.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -from django.contrib import admin -from django.urls import path, include -from rest_framework import routers - -from plugins.views import PluginViewSet -from taskmanager.views import TaskViewSet -from usermanager.views import UserViewSet - -router = routers.DefaultRouter() -router.register(r'v1.0/tasks', TaskViewSet, basename='tasks') -router.register(r'v1.0/users', UserViewSet, basename='users') -router.register(r'v1.0/plugins', PluginViewSet, basename='plugins') - -urlpatterns = [ - path('admin/', admin.site.urls), - path('', include(router.urls)), -] diff --git a/oedp-server/resources/wsgi.py b/oedp-server/resources/wsgi.py deleted file mode 100644 index cda7f6f..0000000 --- a/oedp-server/resources/wsgi.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'resources.settings') - -application = get_wsgi_application() diff --git a/oedp-server/taskmanager/__init__.py b/oedp-server/taskmanager/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/oedp-server/taskmanager/admin.py b/oedp-server/taskmanager/admin.py deleted file mode 100644 index 9759046..0000000 --- a/oedp-server/taskmanager/admin.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -from django.contrib import admin - -# Register your models here. diff --git a/oedp-server/taskmanager/apps.py b/oedp-server/taskmanager/apps.py deleted file mode 100644 index 9d242b7..0000000 --- a/oedp-server/taskmanager/apps.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -from django.apps import AppConfig - - -class TaskmanagerConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'taskmanager' diff --git a/oedp-server/taskmanager/migrations/__init__.py b/oedp-server/taskmanager/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/oedp-server/taskmanager/models.py b/oedp-server/taskmanager/models.py deleted file mode 100644 index 2363313..0000000 --- a/oedp-server/taskmanager/models.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -from django.db import models - - -class Task(models.Model): - - class DeployType(models.TextChoices): - NEW_TASK = 'N', 'new_task' - EXIST_TASK = 'E', 'exist_task' - - class Status(models.IntegerChoices): - NOT_BEGIN = 0 - IN_PROGRESS = 1 - SUCCESS = 2 - FAIL = 3 - - name = models.CharField('任务名称', max_length=64) - deploy_type = models.CharField('部署类型', max_length=1, choices=DeployType.choices) - status = models.IntegerField('任务状态', choices=Status.choices) - start_time = models.DateTimeField('任务开始时间', blank=True, null=True) - end_time = models.DateTimeField('任务结束时间', blank=True, null=True) - user_id = models.IntegerField('用户 id') - is_deleted = models.BooleanField('任务是否被删除', default=False) - created_at = models.DateTimeField('创建时间', auto_now_add=True) - updated_at = models.DateTimeField('更新时间', auto_now=True) - - def __str__(self): - return self.name - - -class TaskPlugin(models.Model): - task_id = models.IntegerField('任务 ID') - plugin_id = models.IntegerField('插件 ID') - created_at = models.DateTimeField('创建时间', auto_now_add=True) - updated_at = models.DateTimeField('更新时间', auto_now=True) - - -class Step(models.Model): - name = models.CharField('阶段名称', max_length=32) - created_at = models.DateTimeField('创建时间', auto_now_add=True) - updated_at = models.DateTimeField('更新时间', auto_now=True) - - -class TaskProcess(models.Model): - - class Status(models.IntegerChoices): - NOT_BEGIN = 0 - IN_PROGRESS = 1 - SUCCESS = 2 - FAIL = 3 - - task_id = models.IntegerField('任务 ID') - step_id = models.IntegerField('阶段 ID') - step_status = models.IntegerField('阶段状态', choices=Status.choices) - created_at = models.DateTimeField('创建时间', auto_now_add=True) - updated_at = models.DateTimeField('更新时间', auto_now=True) - - -class Node(models.Model): - ip = models.CharField('IP 地址', max_length=18) - port = models.IntegerField('端口') - username = models.CharField('用户名', max_length=128) - ciphertext_data = models.CharField('密码密文数据', max_length=512) - arch = models.CharField('节点架构', null=True, blank=True, max_length=32) - os_type = models.CharField('操作系统', null=True, blank=True, max_length=64) - is_deleted = models.BooleanField('节点是否被删除', default=False) - created_at = models.DateTimeField('创建时间', auto_now_add=True) - updated_at = models.DateTimeField('更新时间', auto_now=True) - - def __str__(self): - return f"{self.ip}:{self.port}" - - -class TaskNode(models.Model): - task_id = models.IntegerField('任务 ID') - node_id = models.IntegerField('节点 ID') - node_name = models.CharField('节点名称', max_length=32, help_text="相同节点在不同任务下可以拥有不同名称") - node_role = models.CharField('节点角色', max_length=32, help_text="相同节点在不同任务下可以拥有不同角色") - is_deleted = models.BooleanField('节点使用记录是否被删除', default=False) - created_at = models.DateTimeField('创建时间', auto_now_add=True) - updated_at = models.DateTimeField('更新时间', auto_now=True) diff --git a/oedp-server/taskmanager/serializers.py b/oedp-server/taskmanager/serializers.py deleted file mode 100644 index b33a902..0000000 --- a/oedp-server/taskmanager/serializers.py +++ /dev/null @@ -1,270 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== -import re - -from django.db import transaction -from rest_framework import serializers - -from plugins.models import Plugin -from plugins.serializers import PluginIDSerializer -from taskmanager.models import Task, TaskPlugin, Node, TaskNode -from usermanager.models import User -from utils.cipher import OEDPCipher -from utils.ssh.ssh_connector import SSHConnector, SSHEstablishError, SSHCmdTimeoutError - - -class TaskSerializer(serializers.ModelSerializer): - plugins = serializers.SerializerMethodField() - - def get_plugins(self, obj): - plugin_ids = [entry.plugin_id for entry in TaskPlugin.objects.filter(task_id=obj.id)] - plugin_info = [] - for plugin_id in plugin_ids: - plugin = Plugin.objects.get(pk=plugin_id) - plugin_info.append({'id': plugin.id, 'name': plugin.name}) - return plugin_info - - class Meta: - model = Task - fields = ( - 'id', - 'name', - 'plugins', - ) - - -class TaskSerializerForCreate(serializers.ModelSerializer): - disclaimer = serializers.BooleanField(default=False) - plugins = PluginIDSerializer(many=True) - - class Meta: - model = Task - fields = ( - 'name', - 'deploy_type', - 'disclaimer', - 'plugins', - ) - - def validate(self, data): - task_name = data.get("name") - user_id = self.context.get("request").user.id - # 校验任务名称是否重复 - if Task.objects.filter(name=task_name).filter(is_deleted=False).filter(user_id=user_id).exists(): - raise serializers.ValidationError({"name": f"The task name '{task_name}' already exists."}) - return data - - def validate_disclaimer(self, value): - # 校验免责声明是否同意 - if value is False: - raise serializers.ValidationError('The disclaimer has not been reviewed.') - return value - - def validate_plugins(self, value): - # 校验选择的插件是否存在 - for plugin in value: - plugin_id = plugin.get("id") - if not Plugin.objects.filter(id=plugin_id).exists(): - raise serializers.ValidationError(f'Plugin with ID {plugin_id} does not exist.') - return value - - def create(self, validated_data): - plugin_data = validated_data.pop('plugins') - with transaction.atomic(): - task = Task.objects.create( - name=validated_data.get('name'), - deploy_type=validated_data.get('deploy_type'), - status=Task.Status.NOT_BEGIN, - user_id=self.context.get("request").user.id - ) - task_plugin_entries = [TaskPlugin(task_id=task.id, plugin_id=plugin.get('id')) for plugin in plugin_data] - TaskPlugin.objects.bulk_create(task_plugin_entries) - return task - - -class NodeSerializer(serializers.Serializer): - ip = serializers.CharField() - port = serializers.IntegerField() - username = serializers.CharField() - root_password = serializers.CharField() - password = serializers.CharField() - - def validate_ip(self, value): - # 校验 IP 是否符合规范 - pattern = r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' - if not bool(re.match(pattern, value)): - raise serializers.ValidationError('Invalid IP.') - return value - - def validate_port(self, value): - # 校验端口是否符合规范 - if not 0 < value <= 65535: - raise serializers.ValidationError(f'Invalid port, the valid range for port numbers is 1 to 65535.') - return value - - -class TaskNodeSerializer(serializers.ModelSerializer): - node = serializers.SerializerMethodField() - - class Meta: - model = TaskNode - fields = ( - 'task_id', - 'node_id', - 'node_name', - 'node_role', - 'node' - ) - - @staticmethod - def _get_arch_and_os_type(ssh_connector): - arch = "" - os_type = "" - try: - std, return_code = ssh_connector.execute_cmd("arch") - except (SSHCmdTimeoutError, ValueError): - std = "" - return_code = 1 - if not return_code: - arch = std.strip() - try: - std, return_code = ssh_connector.execute_cmd( - "cat /etc/os-release | grep PRETTY_NAME | cut -d'=' -f2 | tr -d '\"'") - except (SSHCmdTimeoutError, ValueError): - std = "" - return_code = 1 - if not return_code: - os_type = std.strip() - return arch, os_type - - @staticmethod - def _validate_task(task_id, user): - # 校验指定 task_id 的任务是否存在 - try: - task = Task.objects.get(id=task_id) - except Task.DoesNotExist: - raise serializers.ValidationError({'task_id': f'The task with ID {task_id} does not exist.'}) - # 校验用户是否有有权限操作该任务 (管理员用户和该任务的创建者可以操作) - if user.role != User.Role.ADMIN and task.user_id != user.id: - raise serializers.ValidationError( - {'user': f'The current user does not have permission to operate the task with ID {task_id}.'}) - - @staticmethod - def _validate_node_name(task_id, node_name): - # 校验指定任务下是否已存在相同的节点名称 - if TaskNode.objects.filter(node_name=node_name).filter(is_deleted=False).filter(task_id=task_id).exists(): - raise serializers.ValidationError({"node_name": f"The node name '{node_name}' already exists."}) - - @staticmethod - def _encrypt(data): - node = data.get("node") - # 对密码进行加密 - password_dict = { - "root_password": node.pop("root_password"), - "password": node.pop("password") - } - oedp_cipher = OEDPCipher() - node['ciphertext_data'] = oedp_cipher.encrypt_plaintext(password_dict) - del password_dict - return data - - @staticmethod - def _get_ssh_connection(node_dict): - try: - ssh_connector = SSHConnector( - ip=node_dict.get("ip"), - port=node_dict.get("port"), - username=node_dict.get("username"), - ciphertext_data=node_dict.get("ciphertext_data") - ) - except SSHEstablishError as ex: - raise serializers.ValidationError( - {"ssh_connection": [f"Failed to establish SSH connection, Error: {ex}"]} - ) - return ssh_connector - - def _validate_ssh_connection(self, data): - node_dict = data.get("node") - ssh_connector = self._get_ssh_connection(node_dict) - arch, os_type = self._get_arch_and_os_type(ssh_connector) - node_dict["arch"] = arch - node_dict["os_type"] = os_type - return data - - def get_node(self, obj): - node = Node.objects.get(id=obj.node_id) - return { - "ip": node.ip, - "port": node.port, - "username": node.username, - "arch": node.arch, - "os_type": node.os_type - } - - -class TaskNodeSerializerForCreate(TaskNodeSerializer): - node = NodeSerializer() - - class Meta: - model = TaskNode - fields = ( - 'task_id', - 'node_name', - 'node_role', - 'node', - ) - - def validate(self, data): - task_id = data.get('task_id') - user = self.context.get('request').user - node_name = data.get("node_name") - - self._validate_task(task_id, user) - self._validate_node_name(task_id, node_name) - data = self._encrypt(data) - return self._validate_ssh_connection(data) - - def create(self, validated_data): - node_dict = validated_data.pop("node") - ip = node_dict.get("ip") - port = node_dict.get("port") - username = node_dict.get("username") - ciphertext_data = node_dict.get("ciphertext_data") - arch = node_dict.get("arch") - os_type = node_dict.get("os_type") - task_id = validated_data.get("task_id") - node_name = validated_data.get("node_name") - node_role = validated_data.get("node_role") - - nodes = Node.objects.filter(ip=ip).filter(port=port).filter(username=username) - if not nodes: - # 如果节点不存在,则创建节点 - node = Node.objects.create(**node_dict) - else: - # 如果节点存在,则判断密码密文,架构和操作系统信息是否不同,如果不同则更新 - node = nodes[0] - is_save = False - if node.ciphertext_data != ciphertext_data: - node.ciphertext_data = ciphertext_data - is_save = True - if node.arch != arch: - node.arch = arch - is_save = True - if node.os_type != os_type: - node.os_type = os_type - is_save = True - if is_save: - node.save() - task_node = TaskNode.objects.create(task_id=task_id, node_id=node.id, node_name=node_name, node_role=node_role) - return task_node diff --git a/oedp-server/taskmanager/taskscheduler/__init__.py b/oedp-server/taskmanager/taskscheduler/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/oedp-server/taskmanager/taskscheduler/service.py b/oedp-server/taskmanager/taskscheduler/service.py deleted file mode 100644 index 2e05339..0000000 --- a/oedp-server/taskmanager/taskscheduler/service.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -from utils.logger import init_log - -task_logger = init_log("taskmanager.log") - - -def generate_process_timeout_message(node): - """ - 进程超时退出时,生成错误信息,用于刷新数据库 - :param node - :return: - """ - return {} - - -def update_task_completed(message): - """ - 任务调度过程中,若所有节点都完成,更新Task表至完成状态 - :param message: - :return: - """ - pass - - -def schedule_model_service(message): - """ - 任务调度过程中更新对应数据库表 - Node、Step由views层进行初始化,任务调度过程中只做更新 - Report由任务调度进行初始化,并判断是否是重试的任务 - :param message: - :return: - """ - pass diff --git a/oedp-server/taskmanager/taskscheduler/task.py b/oedp-server/taskmanager/taskscheduler/task.py deleted file mode 100644 index 0cd6cf2..0000000 --- a/oedp-server/taskmanager/taskscheduler/task.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -from abc import ABC, abstractmethod -from copy import deepcopy -from multiprocessing import Queue -from typing import NoReturn, Dict - - -class TaskStatus: - """ - 记录各任务的状态 - """ - SUCCESS = 0 - FAILED = 1 - RUNNING = 2 - - @staticmethod - def task_is_completed(status): - """ - 任务是否完成 - :param status: - :return: - """ - return status in (TaskStatus.SUCCESS, TaskStatus.FAILED) - - -class BaseTask(ABC): - """ - 定义任务类的基类,每个节点为一个独立的任务 - """ - - def __init__(self, node: Dict): - self.node = node # node保存节点基本信息,用于调度进程与子进程通信及调度进程对数据库状态的更新 - self.return_message = { - "id": "", - "current_step": "", - "current_step_status": "", - "current_step_status_progress": 0, - "current_step_message": "", - "is_completed": "" - } - - @abstractmethod - def start(self, *args, **kwargs): - """ - 作为各个任务的入口函数,子类必须实现 - :return: - """ - pass - - @abstractmethod - def clear(self, *args, **kwargs): - """ - 作为各个任务的失败清理函数,子类必须实现 - :return: - """ - pass - - def _update_return_message(self, message_queue: Queue, **kwargs) -> NoReturn: - step_status = kwargs.get("step_status") - - if step_status: - self.return_message["step_status"] = step_status - - message_queue.put(deepcopy(self.return_message)) diff --git a/oedp-server/taskmanager/taskscheduler/task_scheduler.py b/oedp-server/taskmanager/taskscheduler/task_scheduler.py deleted file mode 100644 index e31e36a..0000000 --- a/oedp-server/taskmanager/taskscheduler/task_scheduler.py +++ /dev/null @@ -1,406 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -import functools -import multiprocessing -import os -import queue -import threading -import time -import traceback -from concurrent.futures import ThreadPoolExecutor - -from django.db import connections -from utils.record_time.record_time import RecordTime - -from constants.configs.task_config import task_config -from taskmanager.taskscheduler import service -from taskmanager.taskscheduler.task import TaskStatus -from utils.logger import init_log - -task_logger = init_log("taskmanager.log") - -__all__ = ["TASK_SCHEDULER"] - - -def reopen_logger_file(): - """ - 多进程+多线程的架构在fork子进程时其他线程如果正在打印日志会持有日志的文件锁 - fork之后的父子进程文件锁都是持有的,父进程可以正常释放,单子进程无法释放导致日志打印卡死 - 多进程fork后重新打开日志文件,替换掉原来的日志文件对象,防止日志文件锁导致卡住 - """ - for handler in task_logger.handlers: - if hasattr(handler, "stream") and hasattr(handler, "_open"): - handler.stream = handler._open() - - -def keep_while_true(func): - """ - 装饰器,保持while True - :param func: - :return: - """ - - @functools.wraps(func) - def wrapper(*args, **kwargs): - while True: - func(*args, **kwargs) - - return wrapper - - -def task_thread_handler(task_manager): - """ - 开启任务进程 - :param task_manager: - :return: - """ - reopen_logger_file() - connections.close_all() - task_manager.thread_id = threading.get_ident() - ip_address = "127.0.0.1" - port = '22' - if task_manager.task.node.get("node_alias_name", ""): - ip_address = task_manager.task.node.get("ip_address", "127.0.0.1") - port = task_manager.task.node.get("port", '22') - elif task_manager.task.node.get("dst_node_alias_name", ""): - ip_address = task_manager.task.node.get("dst_ip_address", "127.0.0.1") - port = task_manager.task.node.get("dst_port", '22') - actor_name = task_manager.task.__class__.__name__ - task_logger.info("Task start successfully,thread[%s]|ip[%s]|port[%s]|actor_name[%s].", task_manager.thread_id, - ip_address, port, actor_name) - task = task_manager.task - message_queue = task_manager.message_queue - task.start(message_queue) - - -def capture_thread_exception(future_obj): - """ - 捕获线程池运行异常 - :param future_obj: - :return: - """ - task_exception = future_obj.exception() - if task_exception: - ex = traceback.format_exc() - task_logger.error("======ex: %s", ex) - task_logger.error("task run error, error is %s", task_exception) - - -class TaskManager: - """ - 任务调度模块使用的任务类,封装各模块具体的任务实例 - """ - - def __init__(self, task, message_queue): - self.task = task - self.message_queue = message_queue - self.thread = None - self.thread_id = 0 - self.thread_timeout = 0 - self.thread_start_time = None # timezone实例 - - -class ThreadManager: - """ - 线程管理类,封装需要的各种信息 - """ - - def __init__(self, task_thread_pool, message_queue, pending_queue, pending_queue_condition, - running_task_queue, running_task_condition): - self.task_thread_pool = task_thread_pool - self.message_queue = message_queue - self.pending_queue = pending_queue - self.pending_queue_condition = pending_queue_condition - self.running_task_queue = running_task_queue - self.running_task_condition = running_task_condition - - -class ProcessManager: - """ - 进程管理类,封装需要的各种信息 - """ - - def __init__(self, thread_pool_number, pending_queue, pending_queue_condition): - self.process = None - self.thread_pool_number = thread_pool_number - self.pending_queue = pending_queue - self.pending_queue_condition = pending_queue_condition - - -@keep_while_true -def update_task(thread_obj): - """ - 任务状态刷新 - :param thread_obj: 线程管理类对象 - :return: None - """ - reopen_logger_file() - try: - message = thread_obj.message_queue.get(True) - connections.close_all() - # 更新数据库 - is_completed = message.pop("is_completed") - service.schedule_model_service(message) - # 更新时间节点 - RecordTime.update_end_time(message) - # 判断任务是否结束,结束则从运行队列剔除该任务 - check_task_complete(thread_obj, message, is_completed) - except Exception as ex: - task_logger.error(ex) - - -@keep_while_true -def schedule_task(thread_obj): - """ - 任务调度 - :param thread_obj: 线程管理类对象 - :return: None - """ - reopen_logger_file() - task_logger.info("schedule task start") - try: - with thread_obj.pending_queue_condition: - if thread_obj.pending_queue.empty(): - thread_obj.pending_queue_condition.wait() - task_logger.info("schedule task pid is %s", os.getpid()) - task = thread_obj.pending_queue.get() - with thread_obj.running_task_condition: - task_manager = TaskManager(task, thread_obj.message_queue) - task_thread = thread_obj.task_thread_pool.submit(task_thread_handler, task_manager) - task_thread.add_done_callback(capture_thread_exception) - task_manager.thread = task_thread - thread_obj.running_task_queue.put(task_manager) - thread_obj.running_task_condition.notify() - task_logger.info("schedule task finish") - except Exception as ex: - task_logger.error(ex) - - -@keep_while_true -def monitor_task(thread_obj): - """ - 任务进程监控(进程保活已完成,心跳待完成) - :param thread_obj: 线程管理类对象 - :return: None - """ - reopen_logger_file() - try: - check_task_timeout(thread_obj) - except Exception as ex: - task_logger.error(ex) - time.sleep(TaskScheduler.THREAD_MONITORING_DURATION_INTERVAL) - - -def check_task_timeout(thread_obj): - """ - 检查任务是否超时,如果超时,将任务置为任务异常 - :param thread_obj: 线程管理类对象 - :return: None - """ - with thread_obj.running_task_condition: - if thread_obj.running_task_queue.empty(): - thread_obj.running_task_condition.wait() - - for _ in range(thread_obj.running_task_queue.qsize()): - task_manager = thread_obj.running_task_queue.get() - if not task_manager.thread.done(): - thread_obj.running_task_queue.put(task_manager) - continue - if task_manager.thread_timeout >= task_config.thread_timeout: - # 进程异常退出,将数据库任务状态置成任务异常 - connections.close_all() - message = service.generate_process_timeout_message(task_manager.task.node) - service.schedule_model_service(message) - task_logger.error("Task[%s] thread[%s] timeout.", task_manager.task.node.get('id'), task_manager.thread_id) - else: - task_manager.thread_timeout += TaskScheduler.THREAD_MONITORING_DURATION_INTERVAL - thread_obj.running_task_queue.put(task_manager) - - -def check_task_complete(thread_obj, message, complete_status): - """ - 检查任务是否完成,如果完成,将任务从运行队列中移除 - :param thread_obj: 线程管理类对象 - :param message: 任务间通信的消息队列 - :param complete_status: 任务是否完成的标志 - :return: None - """ - with thread_obj.running_task_condition: - if not TaskStatus.task_is_completed(complete_status): - return - task_logger.info("Node[%s] current step[%s] result is: %s.", message.get('id'), message.get('current_step'), - message.get('current_step_status')) - service.update_task_completed(message) - for _ in range(thread_obj.running_task_queue.qsize()): - task_manager = thread_obj.running_task_queue.get() - if task_manager.task.id == message.get("id"): - break - else: - thread_obj.running_task_queue.put(task_manager) - - -def manage_process(process_id): - """ - 管理进程运行,生成线程运行必要环境,创建任务运行线程池 - :param process_id: 进程编号 - :return: None - """ - reopen_logger_file() - task_logger.info("Process start, pid is %s", os.getpid()) - process_manager = None - while not process_manager: - process_manager = TaskScheduler.process_dict.get(process_id) - running_task_queue = queue.Queue() - # 创建通信用的消息队列 - message_queue = queue.Queue() - # 控制运行队列存放和取出的条件变量 - running_task_condition = threading.Condition() - # 创建用来运行任务的线程池 - task_thread_pool = ThreadPoolExecutor(process_manager.thread_pool_number) - # 初始时,子进程中的任务数为0 - thread_obj = ThreadManager(task_thread_pool, message_queue, - process_manager.pending_queue, - process_manager.pending_queue_condition, - running_task_queue, - running_task_condition) - update_task_thread = threading.Thread(target=update_task, args=(thread_obj,), daemon=True) - update_task_thread.start() - monitor_task_thread = threading.Thread(target=monitor_task, args=(thread_obj,), daemon=True) - monitor_task_thread.start() - schedule_task_thread = threading.Thread(target=schedule_task, args=(thread_obj,), daemon=True) - schedule_task_thread.start() - update_task_thread.join() - monitor_task_thread.join() - schedule_task_thread.join() - task_logger.info("Process end, pid is %s", os.getpid()) - - -class TaskScheduler: - """ - 单例实现任务调度模块 - """ - # 线程监控间隔 - THREAD_MONITORING_DURATION_INTERVAL = 2 - # 任务监控间隔 - RUNNING_TASK_DURATION_INTERVAL = 2 - # 最大任务数 - MAX_RUNNING_TASK = task_config.max_task_number - # 单例 - _instance = None - # 机器核数 - cpu_number = multiprocessing.cpu_count() - # 存储子进程的字典 - process_dict = dict() - # 主进程存放任务的队列 - task_save_queue = queue.Queue() - # 控制主线程任务队列的条件变量 - task_save_queue_condition = threading.Condition() - process_num = 0 - - def __new__(cls, *args, **kw): - """ 实例化单例 """ - if cls._instance is None: - cls._instance = object.__new__(cls, *args, **kw) - cls._instance.start() - return cls._instance - - def start(self): - """ - 随单例实例化启动任务调度功能 - :return: None - """ - # 防止获取cpu数量异常导致除数不正确的情况 - if self.cpu_number <= 0: - self.cpu_number = 1 - # 当cpu数量为1时,线程池数为任务数 - if self.cpu_number == 1: - thread_pool_number = self.MAX_RUNNING_TASK - # 当cpu数量大于任务数量时,要创建的进程数变为任务数,每个进程中线程池的数量为1 - elif self.cpu_number > self.MAX_RUNNING_TASK: - self.cpu_number = self.MAX_RUNNING_TASK - thread_pool_number = 1 - # 每个进程的线程池数量为任务数/进程数,如果有余数,那么每个进程中线程池数量多一个 - else: - additional_quantity = 0 - if self.MAX_RUNNING_TASK % self.cpu_number != 0: - additional_quantity = 1 - thread_pool_number = int(self.MAX_RUNNING_TASK / self.cpu_number) + additional_quantity - task_logger.info("process number is %s", self.cpu_number) - task_logger.info("thread number is %s", thread_pool_number) - distribute_task_thread = threading.Thread(target=self.distribute_task, args=()) - distribute_task_thread.daemon = True - distribute_task_thread.start() - self.start_subprocess(thread_pool_number) - task_logger.info("start finish") - - def start_subprocess(self, thread_pool_number): - """ - 子进程管理,生成子进程 - :param thread_pool_number: 子进程中需要开启的线程数量 - :return: None - """ - try: - for i in range(self.cpu_number): - # 生成和子进程通信的等待队列,用来存放需要执行的任务,当主进程有任务添加时,通过此队列将任务分发给子进程 - pending_queue = multiprocessing.Queue() - pending_queue_condition = multiprocessing.Condition() - process_manager = ProcessManager(thread_pool_number, pending_queue, pending_queue_condition) - self.process_dict[i] = process_manager - process_obj = multiprocessing.Process(target=manage_process, args=(i,), daemon=True) - process_manager.process = process_obj - process_obj.start() - except Exception as ex: - task_logger.error(ex) - task_logger.info("create subprocess finish") - - @keep_while_true - def distribute_task(self): - """ - 任务分发,将添加到等待队列中的任务分发给子进程 - :return: None - """ - task_logger.info("distribute task start") - try: - with self.task_save_queue_condition: - if self.task_save_queue.empty(): - self.task_save_queue_condition.wait() - task = self.task_save_queue.get() - # 选择需要分发任务的子进程 - process_manager = self.process_dict.get(self.process_num) - self.process_num = self.process_num + 1 - if self.process_num == self.cpu_number: - self.process_num = 0 - # 将任务分发个选定的子进程 - with process_manager.pending_queue_condition: - process_manager.pending_queue.put(task) - process_manager.pending_queue_condition.notify() - task_logger.info("distribute task finish") - except Exception as ex: - task_logger.error(ex) - - def add_task(self, task): - """ - 添加任务,唤醒任务分发线程 - :param task: 任务actor - :return: None - """ - task_logger.info("add task start") - with self.task_save_queue_condition: - self.task_save_queue.put(task) - self.task_save_queue_condition.notify() - task_logger.info("add task finish") - - -TASK_SCHEDULER = TaskScheduler() diff --git a/oedp-server/taskmanager/tests.py b/oedp-server/taskmanager/tests.py deleted file mode 100644 index 50ef686..0000000 --- a/oedp-server/taskmanager/tests.py +++ /dev/null @@ -1,860 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -import unittest -from unittest.mock import patch - -from rest_framework import serializers -from rest_framework.test import APIClient - -from plugins.models import Plugin -from taskmanager.models import Task, TaskPlugin, Node, TaskNode -from utils.test.testcases import CustomTestCase - - -class TaskTestCase(CustomTestCase): - TASK_URL = "/v1.0/tasks/" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - unittest.TestCase.maxDiff = None - - def _create_a_task(self, name, user_id, task_status=None): - if task_status is None: - task_status = Task.Status.NOT_BEGIN - return Task.objects.create(name=name, status=task_status, user_id=user_id) - - def _create_a_plugin(self, name): - return Plugin.objects.create(name=name) - - def _add_a_node( - self, - node_name, - task_id, - ip="192.168.122.1", - port=22, - node_role="master", - username="root", - ciphertext_data=None - ): - if ciphertext_data is None: - ciphertext_data = "{'half_key': '', 'encrypted_work_key': '', 'work_key_iv': '', " \ - "'ciphertext': '', 'plaintext_iv': ''}" - node = Node.objects.create(ip=ip, port=port, username=username, ciphertext_data=ciphertext_data) - TaskNode.objects.create( - task_id=task_id, node_id=node.id, node_name=node_name, node_role=node_role - ) - - def _create_a_task_plugin_entry(self, task_id, plugin_id): - return TaskPlugin.objects.create(task_id=task_id, plugin_id=plugin_id) - - def _create_tasks(self, task_num, task_name_prefix, user_id, task_status=None): - if task_status is None: - task_status = Task.Status.NOT_BEGIN - for num in range(task_num): - Task.objects.create(name=f'{task_name_prefix}-{num}', status=task_status, user_id=user_id) - - def setUp(self): - # 创建管理源用户 - self.admin = self.create_user("admin") - self.admin_client = APIClient() - self.admin_client.force_authenticate(self.admin) - - def test_task_creation_when_anonymous(self): - """ - 测试匿名状态下创建任务 - 期望结果: - - HTTP 状态码 401 - """ - response = self.anonymous_client.post(self.TASK_URL) - self.assertEqual(response.status_code, 401) - - def test_task_creation_without_params(self): - """ - 测试登录状态但是未输入任何内容的情况下创建任务 - 期望结果: - - HTTP 状态码 400 - - except_data - """ - except_data = { - "is_success": False, - "message": "Please check input.", - "errors": { - "name": [ - "This field is required." - ], - "deploy_type": [ - "This field is required." - ], - "disclaimer": [ - "The disclaimer has not been reviewed." - ], - "plugins": [ - "This field is required." - ] - } - } - - response = self.admin_client.post(self.TASK_URL) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.data, except_data) - - def test_task_creation_with_duplicate_name(self): - """ - 测试使用重复的任务名称创建任务 - 期望结果: - - HTTP 状态码 400 - - except_data - """ - task_name = "Task-01" - user_id = self.admin.id - plugin_name = "K8S" - task = self._create_a_task(task_name, user_id) - plugin = self._create_a_plugin(plugin_name) - self._create_a_task_plugin_entry(task.id, plugin.id) - request_data = { - "name": task_name, - "plugins": [ - { - "id": plugin.id - } - ], - "disclaimer": True, - "deploy_type": "N" - } - except_data = { - "is_success": False, - "message": "Please check input.", - "errors": { - "name": [ - f"The task name '{task_name}' already exists." - ] - } - } - response = self.admin_client.post(self.TASK_URL, request_data, format='json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.data, except_data) - - def test_task_creation_with_invalid_deploy_type(self): - """ - 测试使用无效的 deploy_type 参数创建任务。deploy_type 可选值为 'N' 和 'E',用例中使用 'X' - 期望结果: - - HTTP 状态码 400 - - except_data - """ - plugin = self._create_a_plugin("kubeflow") - request_data = { - "name": "Task-02", - "plugins": [ - { - "id": plugin.id - } - ], - "disclaimer": True, - "deploy_type": "X" - } - except_data = { - "is_success": False, - "message": "Please check input.", - "errors": { - "deploy_type": [ - "\"X\" is not a valid choice." - ] - } - } - response = self.admin_client.post(self.TASK_URL, request_data, format='json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.data, except_data) - - def test_task_creation_with_invalid_plugins(self): - """ - 测试使用无效的 plugins 参数创建任务。plugins 的值的类型为列表,用例中为字典 - 期望结果: - - HTTP 状态码 400 - - except_data: - """ - request_data = { - "name": "Task-03", - "plugins": { - "id": 3 - }, - "disclaimer": True, - "deploy_type": "N" - } - except_data = { - "is_success": False, - "message": "Please check input.", - "errors": { - "plugins": { - "non_field_errors": [ - "Expected a list of items but got type \"dict\"." - ] - } - } - } - response = self.admin_client.post(self.TASK_URL, request_data, format='json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.data, except_data) - - def test_task_creation_with_not_exist_plugins(self): - """ - 测试使用不存在的插件 ID 创建任务 - 期望结果: - - HTTP 状态码 400 - - except_data: - """ - request_data = { - "name": "Task-04", - "plugins": [ - { - "id": 100 - } - ], - "disclaimer": True, - "deploy_type": "N" - } - except_data = { - "is_success": False, - "message": "Please check input.", - "errors": { - "plugins": [ - "Plugin with ID 100 does not exist." - ] - } - } - response = self.admin_client.post(self.TASK_URL, request_data, format='json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.data, except_data) - - def test_task_creation_without_plugin_id(self): - """ - 测试不传 plugins 的 id 时创建任务 - 期望结果: - - HTTP 状态码 400 - - except_data: - """ - request_data = { - "name": "Task-05", - "plugins": [ - { - "idx": 1 - } - ], - "disclaimer": True, - "deploy_type": "N" - } - except_data = { - "is_success": False, - "message": "Please check input.", - "errors": { - "plugins": [ - { - "id": [ - "This field is required." - ] - } - ] - } - } - response = self.admin_client.post(self.TASK_URL, request_data, format='json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.data, except_data) - - def test_task_creation_with_wrong_type_plugin_id(self): - """ - 测试传不为整数类型的 plugins 的 id 时创建任务 - 期望结果: - - HTTP 状态码 400 - - except_data: - """ - request_data = { - "name": "Task-06", - "plugins": [ - { - "id": "a" - } - ], - "disclaimer": True, - "deploy_type": "N" - } - except_data = { - "is_success": False, - "message": "Please check input.", - "errors": { - "plugins": [ - { - "id": [ - "A valid integer is required." - ] - } - ] - } - } - response = self.admin_client.post(self.TASK_URL, request_data, format='json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.data, except_data) - - def test_task_creation_success(self): - """ - 测试成功创建任务 - 期望结果: - - HTTP 状态码 201 - - except_data: - """ - deepseek = self._create_a_plugin("deepseek") - pytorch = self._create_a_plugin("pytorch") - request_data = { - "name": "Task-06", - "plugins": [ - { - "id": deepseek.id - }, - { - "id": pytorch.id - } - ], - "disclaimer": True, - "deploy_type": "N" - } - except_data = { - "is_success": True, - "message": "Create task successfully.", - "data": { - "id": 1, - "name": "Task-06", - "plugins": [ - { - "id": deepseek.id, - "name": "deepseek" - }, - { - "id": pytorch.id, - "name": "pytorch" - } - ] - } - } - response = self.admin_client.post(self.TASK_URL, request_data, format='json') - self.assertEqual(response.status_code, 201) - self.assertEqual(response.data, except_data) - - def test_task_list_when_anonymous(self): - """ - 测试匿名状态下获取任务列表 - 期望结果: - - HTTP 状态码 401 - """ - response = self.anonymous_client.get(self.TASK_URL) - self.assertEqual(response.status_code, 401) - - def test_task_list_with_paginate(self): - """ - 测试使用不同的分页参数获取任务列表 - """ - # 创建 25 条 admin 用户的任务记录,再创建 10 条其他用户的任务记录 - self._create_tasks(25, "Task", self.admin.id) - self._create_tasks(10, "Task_another_user", 2) - - # 测试不传递分页参数,默认获取第 1 页的 10 条记录,期望结果:获取到 10 条记录 - response = self.admin_client.get(self.TASK_URL, format='json') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data.get('data').get('results')), 10) - - # 测试获取第 3 页的 10 条记录,期望结果:获取到 5 条记录 - response = self.admin_client.get(f"{self.TASK_URL}?cur_page=3&page_size=10", format='json') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data.get('data').get('results')), 5) - - # 测试获取第 1 页的 20 条记录,期望结果:获取到 20 条记录 - response = self.admin_client.get(f"{self.TASK_URL}?page_size=20", format='json') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data.get('data').get('results')), 20) - - # 测试获取第 1 页的 50 条记录,期望结果:获取到 25 条记录 - response = self.admin_client.get(f"{self.TASK_URL}?page_size=50", format='json') - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data.get('data').get('results')), 25) - - # 测试获取第 2 页的 50 条记录,期望结果:返回 404 - response = self.admin_client.get(f"{self.TASK_URL}?cur_page=2&page_size=50", format='json') - self.assertEqual(response.status_code, 404) - - def test_task_list_with_filter(self): - """ - 测试传不同的筛选参数时获取任务列表 - """ - # 创建不同任务名称和状态的任务记录 - self._create_tasks(6, "K8S_Task", self.admin.id) - self._create_tasks(2, "K8S_x_Task", self.admin.id, task_status=Task.Status.IN_PROGRESS) - self._create_tasks(8, 'DeepSeek_Task', self.admin.id, task_status=Task.Status.IN_PROGRESS) - self._create_tasks(2, "DeepSeek_R1_Task", self.admin.id, task_status=Task.Status.SUCCESS) - - # 测试查找任务名称中包含 'K8S' 的任务记录,期望结果:获取到 8 条记录 - response = self.admin_client.get(f"{self.TASK_URL}?name=K8S") - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data.get('data').get('results')), 8) - - # 测试查找任务状态为'执行中'的任务记录,期望结果:获取到 10 条记录 - response = self.admin_client.get(f"{self.TASK_URL}?status={Task.Status.IN_PROGRESS}") - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data.get('data').get('results')), 10) - - # 测试查找任务状态为'执行成功'且名字中包含 'Deep' 的任务记录,期望结果:获取到 2 条记录 - response = self.admin_client.get(f"{self.TASK_URL}?status={Task.Status.SUCCESS}&name=Deep") - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data.get('data').get('results')), 2) - - # 查找名字中包含 'task' (忽略大小写) 且单页显示 20 条记录,期望结果:获取到 18 条 - response = self.admin_client.get(f"{self.TASK_URL}?name=task&page_size=20") - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data.get('data').get('results')), 18) - - # 查找名字中包含 'task' (忽略大小写) 且单页默认显示 10 条记录,期望结果:获取到 10 条 - response = self.admin_client.get(f"{self.TASK_URL}?name=task") - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data.get('data').get('results')), 10) - - def test_task_deletion_when_anonymous(self): - """ - 测试匿名状态下删除任务 - 期望结果: - - HTTP 状态码 401 - """ - response = self.anonymous_client.delete(f'{self.TASK_URL}1/') - self.assertEqual(response.status_code, 401) - - def test_task_deletion(self): - """ - 测试正常删除任务 - 期望结果: - - HTTP 状态码 200 - - 对应记录的 is_deleted 字段已经被置为 True - """ - task = self._create_a_task('Task-01', self.admin.id) - response = self.admin_client.delete(f'{self.TASK_URL}{task.id}/') - self.assertEqual(response.status_code, 200) - task.refresh_from_db() - self.assertEqual(task.is_deleted, True) - - def test_node_addition_when_anonymous(self): - """ - 测试匿名状态下添加节点 - 期望结果: - - HTTP 状态码 401 - """ - response = self.anonymous_client.post(self.TASK_URL + 'add_node/') - self.assertEqual(response.status_code, 401) - - def test_node_addition_without_params(self): - """ - 测试登录状态但是未输入任何内容的情况下添加节点 - 期望结果: - - HTTP 状态码 400 - - except_data - """ - except_data = { - "is_success": False, - "message": "Please check input.", - "errors": { - "task_id": [ - "This field is required." - ], - "node_name": [ - "This field is required." - ], - "node_role": [ - "This field is required." - ], - "node": [ - "This field is required." - ], - } - } - - response = self.admin_client.post(self.TASK_URL + 'add_node/') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.data, except_data) - - def test_node_addition_with_duplicate_name(self): - """ - 测试添加节点使用重复的名称 - 期望结果: - - HTTP 状态码 400 - - except_data - """ - task = self._create_a_task("Task-01", self.admin.id) - self._add_a_node("Node-01", task.id) - request_data = { - "task_id": task.id, - "node_name": "Node-01", - "node_role": "master", - "node": { - "ip": "192.168.122.6", - "port": 22, - "username": "root", - "root_password": "Test12#$", - "password": "Test12#$" - } - } - except_data = { - "is_success": False, - "message": "Please check input.", - "errors": { - "node_name": [ - "The node name 'Node-01' already exists." - ] - } - } - - response = self.admin_client.post(self.TASK_URL + 'add_node/', request_data, format='json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.data, except_data) - - def test_node_addition_with_not_exists_task_id(self): - """ - 测试添加节点使用不存在的任务 ID - 期望结果: - - HTTP 状态码 400 - - except_data - """ - request_data = { - "task_id": 1, - "node_name": "Node-01", - "node_role": "master", - "node": { - "ip": "192.168.122.6", - "port": 22, - "username": "root", - "root_password": "Test12#$", - "password": "Test12#$" - } - } - except_data = { - "is_success": False, - "message": "Please check input.", - "errors": { - "task_id": [ - "The task with ID 1 does not exist." - ] - } - } - - response = self.admin_client.post(self.TASK_URL + 'add_node/', request_data, format='json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.data, except_data) - - def test_node_addition_with_invalid_ip_port(self): - """ - 测试添加节点使用无效的 IP 和 端口 - 期望结果: - - HTTP 状态码 400 - - except_data - """ - task = self._create_a_task("Task-01", self.admin.id) - request_data = { - "task_id": task.id, - "node_name": "Node-01", - "node_role": "master", - "node": { - "ip": "192.168.266.6", - "port": "abc", - "username": "root", - "root_password": "Test12#$", - "password": "Test12#$" - } - } - except_data = { - "is_success": False, - "message": "Please check input.", - "errors": { - "node": { - "ip": [ - "Invalid IP." - ], - "port": [ - "A valid integer is required." - ] - } - } - } - response = self.admin_client.post(self.TASK_URL + 'add_node/', request_data, format='json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.data, except_data) - - request_data = { - "task_id": task.id, - "node_name": "Node-01", - "node_role": "master", - "node": { - "ip": "192.168.122.6", - "port": 1000000, - "username": "root", - "root_password": "Test12#$", - "password": "Test12#$" - } - } - except_data = { - "is_success": False, - "message": "Please check input.", - "errors": { - "node": { - "port": [ - "Invalid port, the valid range for port numbers is 1 to 65535." - ] - } - } - } - response = self.admin_client.post(self.TASK_URL + 'add_node/', request_data, format='json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.data, except_data) - - @patch("taskmanager.serializers.TaskNodeSerializer._get_ssh_connection") - def test_node_addition_when_ssh_connection_fail(self, mock_get_ssh_connection): - """ - 测试添加节点信息后,无法建立 SSH 连接,可能原因是 IP 端口不存在,用户名密码错误 - 期望结果: - - HTTP 状态码 400 - - except_data - """ - task = self._create_a_task("Task-01", self.admin.id) - mock_get_ssh_connection.side_effect = serializers.ValidationError( - {"ssh_connection": ["Failed to establish SSH connection."]} - ) - request_data = { - "task_id": task.id, - "node_name": "Node-01", - "node_role": "master", - "node": { - "ip": "192.168.122.6", - "port": 22, - "username": "root", - "root_password": "Test12#$", - "password": "Test12#$" - } - } - except_data = { - "is_success": False, - "message": "Please check input.", - "errors": {"ssh_connection": ["Failed to establish SSH connection."]} - } - response = self.admin_client.post(self.TASK_URL + 'add_node/', request_data, format='json') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.data, except_data) - - @patch("taskmanager.serializers.TaskNodeSerializer._get_arch_and_os_type") - @patch("taskmanager.serializers.TaskNodeSerializer._get_ssh_connection") - def test_node_addition_success(self, mock_get_ssh_connection, mock_get_arch_and_os_type): - """ - 添加节点信息后,成功建立 SSH 连接 - 期望结果: - - HTTP 状态码 201 - - except_data - """ - task = self._create_a_task("Task-01", self.admin.id) - mock_get_ssh_connection.return_value = "ssh_connection" - mock_get_arch_and_os_type.return_value = ("x86_64", "openEuler 22.03 (LTS-SP4)") - request_data = { - "task_id": task.id, - "node_name": "Node-01", - "node_role": "master", - "node": { - "ip": "192.168.122.6", - "port": 22, - "username": "root", - "root_password": "Test12#$", - "password": "Test12#$" - } - } - except_data = { - "is_success": True, - "message": "Add a node successfully.", - "data": { - "task_id": task.id, - "node_id": 1, - "node_name": "Node-01", - "node_role": "master", - "node": { - "ip": "192.168.122.6", - "port": 22, - "username": "root", - "arch": "x86_64", - "os_type": "openEuler 22.03 (LTS-SP4)", - } - } - } - response = self.admin_client.post(self.TASK_URL + 'add_node/', request_data, format='json') - self.assertEqual(response.status_code, 201) - self.assertEqual(response.data, except_data) - - @patch("taskmanager.serializers.TaskNodeSerializer._get_arch_and_os_type") - @patch("taskmanager.serializers.TaskNodeSerializer._get_ssh_connection") - def test_add_same_node_under_diff_task_success(self, mock_get_ssh_connection, mock_get_arch_and_os_type): - """ - 测试不同的任务,成功添加同一个节点 - """ - task_01 = self._create_a_task("Task-01", self.admin.id) - task_02 = self._create_a_task("Task-02", self.admin.id) - mock_get_ssh_connection.return_value = "ssh_connection" - mock_get_arch_and_os_type.return_value = ("x86_64", "openEuler 22.03 (LTS-SP4)") - request_data = { - "task_id": task_01.id, - "node_name": "Node-01", - "node_role": "master", - "node": { - "ip": "192.168.122.6", - "port": 22, - "username": "root", - "root_password": "Test12#$", - "password": "Test12#$" - } - } - except_data = { - "is_success": True, - "message": "Add a node successfully.", - "data": { - "task_id": task_01.id, - "node_id": 1, - "node_name": "Node-01", - "node_role": "master", - "node": { - "ip": "192.168.122.6", - "port": 22, - "username": "root", - "arch": "x86_64", - "os_type": "openEuler 22.03 (LTS-SP4)", - } - } - } - response = self.admin_client.post(self.TASK_URL + 'add_node/', request_data, format='json') - self.assertEqual(response.status_code, 201) - self.assertEqual(response.data, except_data) - - request_data = { - "task_id": task_02.id, - "node_name": "Node-01", - "node_role": "master", - "node": { - "ip": "192.168.122.6", - "port": 22, - "username": "root", - "root_password": "Test12#$", - "password": "Test12#$" - } - } - except_data = { - "is_success": True, - "message": "Add a node successfully.", - "data": { - "task_id": task_02.id, - "node_id": 1, - "node_name": "Node-01", - "node_role": "master", - "node": { - "ip": "192.168.122.6", - "port": 22, - "username": "root", - "arch": "x86_64", - "os_type": "openEuler 22.03 (LTS-SP4)", - } - } - } - response = self.admin_client.post(self.TASK_URL + 'add_node/', request_data, format='json') - self.assertEqual(response.status_code, 201) - self.assertEqual(response.data, except_data) - - @patch("taskmanager.serializers.TaskNodeSerializer._get_arch_and_os_type") - @patch("taskmanager.serializers.TaskNodeSerializer._get_ssh_connection") - def test_add_same_node_under_diff_task_success_02(self, mock_get_ssh_connection, mock_get_arch_and_os_type): - """ - 测试不同的任务添加同一个节点,但是在第二个任务添加节点时,该节点的密码、架构和操作系统类型都已发生变化 - """ - task_01 = self._create_a_task("Task-01", self.admin.id) - task_02 = self._create_a_task("Task-02", self.admin.id) - mock_get_ssh_connection.return_value = "ssh_connection" - mock_get_arch_and_os_type.return_value = ("x86_64", "openEuler 22.03 (LTS-SP4)") - request_data = { - "task_id": task_01.id, - "node_name": "Node-01", - "node_role": "master", - "node": { - "ip": "192.168.122.6", - "port": 22, - "username": "root", - "root_password": "Test12#$", - "password": "Test12#$" - } - } - except_data = { - "is_success": True, - "message": "Add a node successfully.", - "data": { - "task_id": task_01.id, - "node_id": 1, - "node_name": "Node-01", - "node_role": "master", - "node": { - "ip": "192.168.122.6", - "port": 22, - "username": "root", - "arch": "x86_64", - "os_type": "openEuler 22.03 (LTS-SP4)", - } - } - } - response = self.admin_client.post(self.TASK_URL + 'add_node/', request_data, format='json') - self.assertEqual(response.status_code, 201) - self.assertEqual(response.data, except_data) - node = Node.objects.get(id=1) - - mock_get_arch_and_os_type.return_value = ("aarch64", "openEuler 22.03 (LTS-SP3)") - request_data = { - "task_id": task_02.id, - "node_name": "Node-01", - "node_role": "master", - "node": { - "ip": "192.168.122.6", - "port": 22, - "username": "root", - "root_password": "NewTest12#$", - "password": "NewTest12#$" - } - } - except_data = { - "is_success": True, - "message": "Add a node successfully.", - "data": { - "task_id": task_02.id, - "node_id": 1, - "node_name": "Node-01", - "node_role": "master", - "node": { - "ip": "192.168.122.6", - "port": 22, - "username": "root", - "arch": "aarch64", - "os_type": "openEuler 22.03 (LTS-SP3)", - } - } - } - response = self.admin_client.post(self.TASK_URL + 'add_node/', request_data, format='json') - self.assertEqual(response.status_code, 201) - self.assertEqual(response.data, except_data) - node_ = Node.objects.get(id=1) - - # 判断节点信息是否已更新 - self.assertNotEqual(node.ciphertext_data, node_.ciphertext_data) - self.assertNotEqual(node.arch, node_.arch) - self.assertNotEqual(node.os_type, node_.os_type) - self.assertEqual(node_.arch, "aarch64") - self.assertEqual(node_.os_type, "openEuler 22.03 (LTS-SP3)") diff --git a/oedp-server/taskmanager/views.py b/oedp-server/taskmanager/views.py deleted file mode 100644 index cf0ef9e..0000000 --- a/oedp-server/taskmanager/views.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -from rest_framework import viewsets, status -from rest_framework.decorators import action -from rest_framework.response import Response - -from taskmanager.models import Task -from taskmanager.serializers import ( - TaskSerializer, - TaskSerializerForCreate, - TaskNodeSerializerForCreate, - TaskNodeSerializer -) -from utils.logger import init_log -from utils.ssh.ssh_connector import SSHConnector, SSHEstablishError, SSHCmdTimeoutError - -run_logger = init_log("run.log") - - -class TaskViewSet(viewsets.GenericViewSet): - queryset = Task.objects.all() - - @staticmethod - def _filter_tasks(request, task_queryset): - name = request.query_params.get("name") - task_status = request.query_params.get("status") - if name: - task_queryset = task_queryset.filter(name__icontains=name) - if task_status: - task_queryset = task_queryset.filter(status=task_status) - return task_queryset - - def list(self, request): - task_queryset = Task.objects.filter(is_deleted=False).filter(user_id=request.user.id) - tasks = self.paginate_queryset(self._filter_tasks(request, task_queryset)) - serializer = TaskSerializer( - tasks, - context={'request': request}, - many=True, - ) - return self.get_paginated_response(serializer.data) - - def create(self, request): - serializer = TaskSerializerForCreate(data=request.data, context={"request": request}) - if not serializer.is_valid(): - return Response({ - 'is_success': False, - 'message': 'Please check input.', - 'errors': serializer.errors - }, status=status.HTTP_400_BAD_REQUEST) - task = serializer.save() - return Response({ - 'is_success': True, - 'message': 'Create task successfully.', - 'data': TaskSerializer(task).data - }, status=status.HTTP_201_CREATED) - - def destroy(self, request, pk): - task = Task.objects.get(id=pk) - task.is_deleted = True - task.save() - return Response({ - 'is_success': True, - 'message': 'Delete task successfully.', - 'data': TaskSerializer(task).data - }, status=status.HTTP_200_OK) - - @action(methods=['POST'], detail=False) - def add_node(self, request): - serializer = TaskNodeSerializerForCreate(data=request.data, context={'request': request}) - if not serializer.is_valid(): - return Response({ - 'is_success': False, - 'message': 'Please check input.', - 'errors': serializer.errors - }, status=status.HTTP_400_BAD_REQUEST) - task_node_entry = serializer.save() - data = TaskNodeSerializer(task_node_entry).data - return Response({ - 'is_success': True, - 'message': 'Add a node successfully.', - 'data': data - }, status=status.HTTP_201_CREATED) diff --git a/oedp-server/usermanager/__init__.py b/oedp-server/usermanager/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/oedp-server/usermanager/apps.py b/oedp-server/usermanager/apps.py deleted file mode 100644 index 2437c7c..0000000 --- a/oedp-server/usermanager/apps.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-21 -# ====================================================================================================================== - -from django.apps import AppConfig - - -class UsermanagerConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'usermanager' diff --git a/oedp-server/usermanager/jwt_auth/__init__.py b/oedp-server/usermanager/jwt_auth/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/oedp-server/usermanager/jwt_auth/authentication.py b/oedp-server/usermanager/jwt_auth/authentication.py deleted file mode 100644 index ad839cd..0000000 --- a/oedp-server/usermanager/jwt_auth/authentication.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-24 -# ====================================================================================================================== -from datetime import datetime - -import jwt -import pytz -from django.contrib.auth import get_user_model -from django.utils.encoding import smart_text -from rest_framework.authentication import BaseAuthentication -from rest_framework.exceptions import AuthenticationFailed - -from constants.auth import JWT_AUTH_HEADER_PREFIX -from usermanager.jwt_auth.jwt_manager import JWTManager -from utils.time import get_time_zone - - -class TokenAuthentication(BaseAuthentication): - - def __init__(self): - self.user = None - super().__init__() - - def _get_user(self, user_id): - user_model = get_user_model() - try: - self.user = user_model.objects.get(pk=user_id) - except user_model.DoesNotExist: - raise AuthenticationFailed('User not exists.') - - def _get_token(self, request): - # 从 cookie 中获取 token - token = request.COOKIES.get('token', '') - if isinstance(token, str): - # Work around django test client oddness - token = token.encode('iso-8859-1') - auth_header = token.split() - if not token or smart_text(auth_header[0].lower()) != JWT_AUTH_HEADER_PREFIX.lower(): - return None - if len(auth_header) != 2: - raise AuthenticationFailed('Invalid Authorization header.') - return auth_header[1] - - def _check_token(self, token): - try: - payload = JWTManager().decode_token(token) - # 签名过期 - except jwt.ExpiredSignatureError as error: - raise AuthenticationFailed('Token has expired.') from error - # 解码错误 - except jwt.DecodeError as error: - raise AuthenticationFailed('Decoding token error.') from error - # 无效token - except jwt.InvalidTokenError as error: - raise AuthenticationFailed('Invalid token.') from error - user_id = payload.get('user_id', None) - username = payload.get("username", "") - if not (username and user_id): - raise AuthenticationFailed('Invalid payload.') - self._get_user(user_id) - - def _check_csrf_token(self, csrf_token): - if not self.user.csrf_token: - raise AuthenticationFailed('User not logged in.') - if self.user.csrf_token != csrf_token: - raise AuthenticationFailed('Invalid CSRF token.') - current_datetime = datetime.now(tz=pytz.timezone(get_time_zone())) - if current_datetime > self.user.expires_at: - raise AuthenticationFailed('CSRF token has expired.') - - def authenticate(self, request): - token = self._get_token(request) - csrf_token = request.COOKIES.get('csrf_token', '') - if token is None or csrf_token is None: - return None - self._check_token(token) - self._check_csrf_token(csrf_token) - return self.user, None - - def authenticate_header(self, request): - """ - 认证 jwt 头部 - """ - return f'{JWT_AUTH_HEADER_PREFIX} realm="api"' diff --git a/oedp-server/usermanager/jwt_auth/jwt_manager.py b/oedp-server/usermanager/jwt_auth/jwt_manager.py deleted file mode 100644 index 270a6e7..0000000 --- a/oedp-server/usermanager/jwt_auth/jwt_manager.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-23 -# ====================================================================================================================== - -import uuid -from datetime import datetime, timedelta - -import jwt -import pytz -from django.contrib.auth import get_user_model -from jwt.utils import base64url_encode - -from constants.auth import JWT_SECRET_KEY, JWT_ALGORITHM, JWT_EXPIRY_DAYS -from utils.cipher import generate_random_bytes -from utils.time import get_time_zone - - -class JWTManager: - - @staticmethod - def _get_username(user): - """ - 获取用户名 - """ - try: - username = user.get_username() - except AttributeError: - username = user.username - return username - - def _generate_payload(self, user): - """ - 生成 payload - """ - username_field = get_user_model().USERNAME_FIELD - username = self._get_username(user) - expiration_time = datetime.now(tz=pytz.timezone(get_time_zone())) + timedelta(days=JWT_EXPIRY_DAYS) - return {'user_id': user.pk, username_field: username, 'exp': expiration_time} - - def decode_token(self, token): - options = {'verify_exp': False} - payload = jwt.decode( - token, - JWT_SECRET_KEY, - algorithms=[JWT_ALGORITHM], - options=options, - audience=None, - leeway=0, - issuer=None, - ) - return payload - - def generate_token(self, user): - """ - 生成 token - """ - payload = self._generate_payload(user) - token = "JWT {}".format(jwt.encode(payload, JWT_SECRET_KEY, JWT_ALGORITHM)) - return token - - @staticmethod - def generate_csrf_token(): - """ - 生成 csrf-token - """ - csrf_token = base64url_encode(generate_random_bytes(32)) - return csrf_token \ No newline at end of file diff --git a/oedp-server/usermanager/middlewares.py b/oedp-server/usermanager/middlewares.py deleted file mode 100644 index dd60e0d..0000000 --- a/oedp-server/usermanager/middlewares.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-05 -# ====================================================================================================================== -from usermanager.jwt_auth.jwt_manager import JWTManager - - -class TokenMiddleware: - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - response = self.get_response(request) - user = response.data.get("user") - - return response diff --git a/oedp-server/usermanager/migrations/__init__.py b/oedp-server/usermanager/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/oedp-server/usermanager/models.py b/oedp-server/usermanager/models.py deleted file mode 100644 index 2529848..0000000 --- a/oedp-server/usermanager/models.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-21 -# ====================================================================================================================== - -from django.apps import apps -from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager -from django.contrib.auth.hashers import make_password -from django.db import models - -from utils.cipher import get_salt - - -class CustomUserManager(BaseUserManager): - - def _create_user(self, username, password, **extra_fields): - if not username: - raise ValueError("The given username must be set") - GlobalUserModel = apps.get_model( - self.model._meta.app_label, self.model._meta.object_name - ) - username = GlobalUserModel.normalize_username(username) - user = self.model(username=username, **extra_fields) - user.password = make_password(password, salt=get_salt()) - user.save(using=self._db) - return user - - def create_user(self, username, password=None, **extra_fields): - extra_fields.setdefault("has_reset", False) - return self._create_user(username, password, **extra_fields) - - -class User(AbstractBaseUser): - - class RoleChoices(models.IntegerChoices): - ADMIN = 0 - USER = 1 - GUEST = 2 - - USERNAME_FIELD = 'username' - REQUIRED_FIELDS = ['role'] - - username = models.CharField('用户名', max_length=32, unique=True) - password = models.CharField('用户密码', max_length=32, blank=True, null=True) - role = models.IntegerField('用户角色', choices=RoleChoices.choices) - has_reset = models.BooleanField('用户是否重设密码', default=False, blank=True, null=True) - last_login = models.DateTimeField('上次登陆时间', blank=True, null=True) - - csrf_token = models.CharField('CSRF token', max_length=255, blank=True, null=True) - expires_at = models.DateTimeField('CSRF token 失效时间', blank=True, null=True) - - created_at = models.DateTimeField('创建时间', auto_now_add=True) - updated_at = models.DateTimeField('更新时间', auto_now=True) - - objects = CustomUserManager() - - def __str__(self): - return self.username - - def set_password(self, new_password): - """ - 设置密码,使用自定义方法生成的盐值 - """ - # 长度为18的字节数组base64后会转成长度为24的字符串 - self.password = make_password(new_password, salt=get_salt()) - self._password = new_password diff --git a/oedp-server/usermanager/serializers.py b/oedp-server/usermanager/serializers.py deleted file mode 100644 index bb074cd..0000000 --- a/oedp-server/usermanager/serializers.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-21 -# ====================================================================================================================== - -import re -from datetime import datetime, timedelta - -from django.contrib.auth.hashers import make_password -from rest_framework import serializers - -from constants.auth import JWT_EXPIRY_DAYS -from constants.configs.account_config import ADMIN_ID, USERNAME_MIN_LEN, USERNAME_MAX_LEN -from usermanager.models import User -from utils.cipher import get_salt - - -def validate_password_valid(value): - """ - 校验器函数,检查密码的合法性,密码需要包括大小写字母、数字和特殊字符中的两种以上,且长度为 8-32 位 - """ - regex = r"^(?![A-Z]+$)(?![a-z]+$)(?![0-9]+$)" \ - r"(?![`~!@#$%^&*()\-_=+\\|[{}\];:'\",<.>/?]+$)" \ - r"[A-Za-z0-9`~!@#$%^&*()\-_=+\\|[{}\];:'\",<.>/?]{8,32}$" - match = re.match(regex, value) - if not match: - del value, match - raise serializers.ValidationError('The password is invalid.') - del value, match - - -def confirm_password(data): - """ - 确认两次输入的密码是否匹配 - """ - password = data.get('password') - confirmed_password = data.get('confirmed_password') - if password != confirmed_password: - del password, confirmed_password - raise serializers.ValidationError({'confirmed_password': 'The passwords do not match.'}) - del password, confirmed_password - - -def check_username_as_password(data): - """ - 检查是否把用户名作为密码 - """ - username = data.get('username') - password = data.get('password') - if password == username or password == username[::-1]: - del password - raise serializers.ValidationError({'password': 'The username cannot be used as the password.'}) - del password - - -class UserSerializer(serializers.ModelSerializer): - - class Meta: - model = User - fields = ( - 'id', - 'username', - 'role', - ) - - -class UserSerializerForCreate(serializers.ModelSerializer): - password = serializers.CharField(max_length=32, min_length=8, validators=[validate_password_valid]) - confirmed_password = serializers.CharField(max_length=32, min_length=8) - - class Meta: - model = User - fields = ( - 'username', - 'password', - 'confirmed_password', - 'role', - ) - - @staticmethod - def _check_username_valid(username): - """ - 检查用户名的合法性,用户名需以字母开头的,可以包含字母、数字或特殊字符(-_),且长度为 6-32 位 - """ - regex = "^[a-zA-Z][a-zA-Z0-9_-]{5,31}$" - match = re.match(regex, username) - if match: - return True - return False - - def validate(self, data): - # 校验两次输入的密码是否相等 - confirm_password(data) - # 校验是否把用户名作为密码 - check_username_as_password(data) - # TODO 校验密码是否为已配置的弱密码 - return data - - def validate_username(self, value): - if not self._check_username_valid(value): - raise serializers.ValidationError('The username is invalid.') - if User.objects.filter(username=value).exists(): - raise serializers.ValidationError(f'The username "${value}" already exists.') - return value - - @staticmethod - def validate_role(value): - if value is None or value not in (User.RoleChoices.USER, User.RoleChoices.GUEST): - raise serializers.ValidationError('The role is invalid.') - return value - - def create(self, validated_data): - username = validated_data.get('username', '') - password = validated_data.get('password', '') - return User.objects.create_user(username, password, **validated_data) - - -class UserSerializerForResetPW(serializers.ModelSerializer): - password = serializers.CharField(max_length=32, min_length=8, validators=[validate_password_valid]) - confirmed_password = serializers.CharField(max_length=32, min_length=8) - - class Meta: - model = User - fields = ( - 'id', - 'username', - 'password', - 'confirmed_password', - ) - - def validate(self, data): - # 校验用户 id 是否和管理员 id 相同。 - if int(data.get('id')[0]) != ADMIN_ID: - raise serializers.ValidationError({'id': 'This id is not the id of the administrator user.'}) - # 判断用户是否存在 - if not User.objects.filter(id=ADMIN_ID).exists(): - raise serializers.ValidationError({'id', 'The user whose id is 1 not exists.'}) - # 判断用户是否是管理员 - if User.objects.get(id=ADMIN_ID).role != User.RoleChoices.ADMIN: - raise serializers.ValidationError('The role of user is not administrator.') - # 判断是否重置过密码 - if User.objects.get(id=ADMIN_ID).has_reset: - raise serializers.ValidationError('The Admin user has reset password.') - # 校验两次输入的密码是否相等 - confirm_password(data) - # 校验是否把用户名作为密码 - check_username_as_password(data) - return data - - def update(self, instance, validated_data): - instance.password = make_password(validated_data['password'], salt=get_salt()) - instance.has_reset = True - instance.save() - return instance - - -class UserSerializerForLogin(serializers.ModelSerializer): - disclaimer = serializers.BooleanField(default=False) - - class Meta: - model = User - fields = ( - 'username', - 'password', - 'disclaimer' - ) - - def validate_disclaimer(self, value): - if value is False: - raise serializers.ValidationError('The disclaimer has not been reviewed.') - return value - - def validate_username(self, value): - if not USERNAME_MIN_LEN <= len(value) <= USERNAME_MAX_LEN: - raise serializers.ValidationError(f'The username must be between {USERNAME_MIN_LEN} and {USERNAME_MAX_LEN} ' - 'characters in length.') - if not User.objects.filter(username=value).exists(): - raise serializers.ValidationError(f'The user {value} not exists.') - return value - - def validate(self, data): - user = User.objects.get(username=data.get('username')) - if user.has_reset is False and user.id == ADMIN_ID: - raise serializers.ValidationError('Admin user needs to reset password before login.') - return data - - def update(self, instance, validated_data): - instance.csrf_token = validated_data.get('csrf_token') - instance.expires_at = datetime.now() + timedelta(days=JWT_EXPIRY_DAYS) - instance.last_login = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - instance.save() - return instance diff --git a/oedp-server/usermanager/views.py b/oedp-server/usermanager/views.py deleted file mode 100644 index ae9fab9..0000000 --- a/oedp-server/usermanager/views.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-21 -# ====================================================================================================================== - -from django.contrib.auth import authenticate, login, logout -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.response import Response -from rest_framework.viewsets import ViewSet - -from constants.configs.account_config import ADMIN_ID -from usermanager.jwt_auth.jwt_manager import JWTManager -from usermanager.models import User -from usermanager.serializers import ( - UserSerializer, - UserSerializerForResetPW, - UserSerializerForLogin, -) - - -class UserViewSet(ViewSet): - - @action(methods=['PUT'], detail=False, authentication_classes=[]) - def reset_password(self, request): - """ - 在管理员用户首次登陆前修改密码 - """ - try: - admin = User.objects.get(id=ADMIN_ID) - except User.DoesNotExist: - return Response({ - "is_success": False, - "message": "Please check admin user.", - "errors": { - 'id': [f'The user whose id is {ADMIN_ID} does not exist.'] - } - }, status=status.HTTP_400_BAD_REQUEST) - serializer = UserSerializerForResetPW(admin, data=request.data, partial=True) - if not serializer.is_valid(): - return Response({ - "is_success": False, - "message": "Please check input.", - "errors": serializer.errors, - }, status=status.HTTP_400_BAD_REQUEST) - user = serializer.save() - return Response({ - "is_success": True, - "message": "Reset password successfully.", - "data": UserSerializer(user).data, - }, status=status.HTTP_201_CREATED) - - @action(methods=['POST'], detail=False, authentication_classes=[]) - def login(self, request): - user = User.objects.get(username=request.data.get('username')) - serializer = UserSerializerForLogin(user, data=request.data) - if not serializer.is_valid(): - return Response({ - "is_success": False, - "message": "Please check input.", - "errors": serializer.errors, - }, status=status.HTTP_400_BAD_REQUEST) - username = serializer.validated_data['username'] - password = serializer.validated_data['password'] - user = authenticate(username=username, password=password) - del password - if not user or user.is_anonymous: - return Response({ - "is_success": False, - "message": "Username and password does not match.", - "errors": {} - }, status=400) - login(request, user) - response = Response({ - "is_success": True, - "message": "Login successfully.", - "data": UserSerializer(user).data, - }, status=status.HTTP_200_OK) - # 设置 JWT token 和 CSRF token - jwt_manager = JWTManager() - token = jwt_manager.generate_token(user) - csrf_token = jwt_manager.generate_csrf_token() - response.set_cookie('csrf_token', csrf_token) - response.set_cookie("token", token) - serializer.save(csrf_token=csrf_token) - return response - - @action(methods=['POST'], detail=False) - def logout(self, request): - user = request.user - user.csrf_token = "" - user.expires_at = None - user.save() - logout(request) - return Response({ - "is_success": True, - "message": "Logout successfully.", - "data": [] - }, status=status.HTTP_200_OK) diff --git a/oedp-server/utils/__init__.py b/oedp-server/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/oedp-server/utils/cipher.py b/oedp-server/utils/cipher.py deleted file mode 100644 index 5fa848b..0000000 --- a/oedp-server/utils/cipher.py +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-16 -# ====================================================================================================================== -import binascii -import hashlib -import json -import secrets -from base64 import b64decode, b64encode -from typing import Union - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - -from constants.configs.account_config import SALT_LENGTH - - -class DecryptError(Exception): - - def __int__(self, message): - self._message = message - - def __str__(self): - return self._message - - -class OEDPCipher: - - @staticmethod - def _generate_root_key(half_key_1: str) -> bytes: - """ - 通过组合两个半秘钥,然后加上盐值,使用 PBKDF2 算法进行哈希计算,最终返回一个根秘钥 - """ - half_key_2 = "fl1HGmk3k45xza89" - # 组合秘钥,并转换为 utf-8 格式的字符串 - key = (half_key_1 + half_key_2).encode("utf-8") - salt = b"k\x80bb\xd4\xb1(\x87\xd6\x19;\x8dX\x91^1\xe2\x91\xb6\xf6" - # 加密秘钥 - encrypted_key = hashlib.pbkdf2_hmac("sha256", key, salt, 16582) - # 将二进制转换为十六进制,然后截取部分后返回 - return binascii.hexlify(encrypted_key)[13:45] - - @staticmethod - def _encrypt(key: bytes, iv: bytes, plaintext: bytes) -> bytes: - """ - 使用 AES-GCM 算法对给定明文进行加密。 - :param key: AES-GCM 算法使用的密钥 - :param iv: 初始化向量 - :param plaintext: 需要加密的明文字节串 - """ - # 创建加密器 - encryptor = Cipher(algorithms.AES(key), modes.GCM(iv), default_backend()).encryptor() - # 加密明文 - ciphertext = encryptor.update(plaintext) + encryptor.finalize() - return ciphertext - - @staticmethod - def _decrypt(key: bytes, iv: bytes, ciphertext: bytes) -> bytes: - """ - 使用 AES-GCM 算法对给定密文进行解密。 - :param key: AES-GCM 算法使用的密钥 - :param iv: 初始化向量 - :param ciphertext: 需要解密的明文字节串 - """ - # 创建解密器 - decrypter = Cipher(algorithms.AES(key), modes.GCM(iv), default_backend()).decryptor() - # 解密密文 - plaintext = decrypter.update(ciphertext) - return plaintext - - def _generate_work_key(self, half_key: str) -> (str, str, bytes): - """ - 随机生成一个工作秘钥和初始化向量,然后通过根秘钥对工作秘钥进行加密, - 最后将加密后的工作秘钥和初始化向量以 Base64 编码的字符串形式返回, - 为加密的工作秘钥以字节串的形式返回。 - """ - # 获取根秘钥 - root_key = self._generate_root_key(half_key) - # 生成32字节的安全随机字节串作为工作秘钥,16字节的作为初始化向量 - work_key = secrets.token_bytes(32) - work_key_iv = secrets.token_bytes(16) - # 加密工作秘钥 - encrypted_work_key = self._encrypt(root_key, work_key_iv, work_key) - # 编码为 Base64 格式的字节串,方便传输。然后解码为 ASCII 字符串 - encrypted_work_key = b64encode(encrypted_work_key).decode("ascii") - work_key_iv = b64encode(work_key_iv).decode("ascii") - return encrypted_work_key, work_key_iv, work_key - - def _decrypt_work_key(self, half_key: str, encrypted_work_key: str, work_key_iv: str) -> bytes: - """ - 解密工作秘钥 - """ - root_key = self._generate_root_key(half_key) - encrypted_work_key = b64decode(encrypted_work_key.encode("ascii")) - work_key_iv = b64decode(work_key_iv.encode("ascii")) - return self._decrypt(root_key, work_key_iv, encrypted_work_key) - - def encrypt_plaintext(self, plaintext: Union[str, dict]) -> dict: - """ - 加密明文, 明文可以是字符串或者字典 - """ - plaintext = json.dumps(plaintext) - half_key = self.generate_random_string() - encrypted_work_key, work_key_iv, work_key = self._generate_work_key(half_key) - plaintext_with_salt = f"{half_key}{plaintext}".encode("utf-8") - del plaintext - plaintext_iv = secrets.token_bytes(16) - ciphertext = self._encrypt(work_key, plaintext_iv, plaintext_with_salt) - del plaintext_with_salt - ciphertext = b64encode(ciphertext).decode("ascii") - plaintext_iv = b64encode(plaintext_iv).decode("ascii") - ciphertext_data = { - 'half_key': half_key, - 'encrypted_work_key': encrypted_work_key, - 'work_key_iv': work_key_iv, - 'ciphertext': ciphertext, - 'plaintext_iv': plaintext_iv, - } - return ciphertext_data - - def decrypt_ciphertext_data(self, ciphertext_data: dict) -> Union[str, dict]: - """ - 解密密文 - """ - half_key = ciphertext_data.get('half_key', '') - encrypted_work_key = ciphertext_data.get('encrypted_work_key', '') - work_key_iv = ciphertext_data.get('work_key_iv', '') - ciphertext = ciphertext_data.get('ciphertext', '') - plaintext_iv = ciphertext_data.get('plaintext_iv', '') - # 当 ciphertext_data 字典的某一个 key 的值为空时,解密失败,抛出异常 - if not all([half_key, encrypted_work_key, work_key_iv, ciphertext, plaintext_iv]): - raise DecryptError('Failed to decrypt, invalid ciphertext json') - work_key = self._decrypt_work_key(half_key, encrypted_work_key, work_key_iv) - ciphertext = b64decode(ciphertext.encode("ascii")) - plaintext_iv = b64decode(plaintext_iv.encode("ascii")) - plaintext_with_salt = self._decrypt(work_key, plaintext_iv, ciphertext) - plaintext_with_salt = plaintext_with_salt.decode("utf-8") - plaintext = plaintext_with_salt[len(half_key):] - return json.loads(plaintext) - - @staticmethod - def generate_random_string(length=16, seed=None) -> str: - """ - 根据指定的字符集,生成指定长度的随机字符串 - :return: - """ - if seed is None: - seed = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - return "".join(secrets.choice(seed) for _ in range(length)) - - @staticmethod - def get_salt(length=SALT_LENGTH): - return b64encode(secrets.token_bytes(length)).decode() diff --git a/oedp-server/utils/cmd_executor.py b/oedp-server/utils/cmd_executor.py deleted file mode 100644 index af6686e..0000000 --- a/oedp-server/utils/cmd_executor.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-05 -# ====================================================================================================================== - -import os -import signal -import subprocess -import sys - -ERROR_CODE = 1 -TIMEOUT_CODE = 2 - - -class CommandExecutor: - - def __init__(self, cmd, encoding=sys.getdefaultencoding(), timeout=300): - self.process = subprocess.Popen( - cmd, universal_newlines=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, start_new_session=True, - encoding=encoding - ) - self.cmd = cmd - self.timeout = timeout - - def run(self): - try: - stdout, stderr = self.process.communicate(timeout=self.timeout) - except subprocess.TimeoutExpired: - # 终止超时进程 - self.process.kill() - self.process.terminate() - os.killpg(self.process.pid, signal.SIGTERM) - return "", "", TIMEOUT_CODE - except Exception as ex: - # 终止异常进程 - self.process.kill() - self.process.terminate() - os.killpg(self.process.pid, signal.SIGTERM) - return "", "", ERROR_CODE - return stdout, stderr, self.process.returncode diff --git a/oedp-server/utils/config_parser.py b/oedp-server/utils/config_parser.py deleted file mode 100644 index 23b2ee3..0000000 --- a/oedp-server/utils/config_parser.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-16 -# ====================================================================================================================== -import configparser -import os - - -class ConfParser: - - def __init__(self, config_file, logger=None): - self.logger = logger - self.config_file = config_file - self.config = configparser.ConfigParser() - self.config.read(config_file, encoding='utf-8') - - def _check_config_file(self): - if not os.path.exists(self.config_file): - if not self.logger: - self.logger.warning(f"The configuration file {self.config_file} not found") - return False - if not os.access(self.config_file, os.R_OK): - if not self.logger: - self.logger.warning(f'Can not read configuration file {self.config_file}: Permission denied') - return False - return True - - def get(self, section, key, default=None): - if not self._check_config_file(): - return default - try: - value = self.config.get(section, key, fallback=default) - except configparser.ParsingError as ex: - if not self.logger: - self.logger.warning( - f'The content of the configuration file {self.config_file} is incorrect, error: {ex}') - value = default - return value diff --git a/oedp-server/utils/file_handler/__init__.py b/oedp-server/utils/file_handler/__init__.py deleted file mode 100644 index c1f18ff..0000000 --- a/oedp-server/utils/file_handler/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-14 -# ====================================================================================================================== diff --git a/oedp-server/utils/file_handler/base_handler.py b/oedp-server/utils/file_handler/base_handler.py deleted file mode 100644 index c094e05..0000000 --- a/oedp-server/utils/file_handler/base_handler.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-14 -# ====================================================================================================================== - -import os - - -class FileError(Exception): - - def __int__(self, msg): - self.msg = msg - - def __str__(self): - return self.msg - - -class BaseHandler: - - def __init__(self, file_path=None, logger=None, should_print=False): - self.file_path = file_path - self.logger = logger - if self.logger: - should_print = False - self.should_print = should_print - - def _check_file_path(self) -> bool: - """ - 检查文件路径是否存在,存在则返回 True - """ - if not os.path.exists(self.file_path): - msg = f"The file {self.file_path} not found" - if self.logger: - self.logger.warning(msg) - if self.should_print: - print(msg) - return False - return True - - def _check_file_permission(self, mode=os.R_OK) -> bool: - """ - 检查是否有操作文件的权限, 有权限则返回 True - """ - if not os.access(self.file_path, mode): - msg = f'Can not operate file {self.file_path}: Permission denied' - if self.logger: - self.logger.warning(msg) - if self.should_print: - print(msg) - return False - return True diff --git a/oedp-server/utils/file_handler/conf_handler.py b/oedp-server/utils/file_handler/conf_handler.py deleted file mode 100644 index cd43bdd..0000000 --- a/oedp-server/utils/file_handler/conf_handler.py +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-16 -# ====================================================================================================================== - -import configparser -import os -from collections import OrderedDict -from typing import Union - -from utils.file_handler.base_handler import BaseHandler, FileError - - -class ConfHandler(BaseHandler): - - def __init__(self, **kwargs): - """ - 初始化 - :param kwargs: - file_path 文件路径 - logger 日志记录器,当该参数不为 None 时,打印日志 - should_print 是否打印提示,当 logger 参数为 None 时才会生效,True 表示打印提示信息 - :exception: - FileError: 配置文件校验失败时抛出 - configparser.MissingSectionHeaderError: 配置文件格式异常,文件第一行不是 section 时抛出 - configparser.ParsingError: 配置文件格式异常,不符合 .conf 文件格式时抛出 - """ - super().__init__(**kwargs) - self.config = configparser.ConfigParser(dict_type=OrderedDict) - # 设置大小写敏感 - self.config.optionxform = str - if not (self._check_file_path() and self._check_file_permission()): - raise FileError('Failed to check file.') - try: - self.config.read(self.file_path, encoding='utf-8') - except (configparser.MissingSectionHeaderError, configparser.ParsingError) as ex: - if self.logger: - self.logger.warning(ex) - if self.should_print: - print(ex) - raise ex - - def get(self, section: str, option: str, default: str = None) -> str: - """ - 获取指定的 section 和 option 的值。 - 如果 section 和 option 不存在,或者在配置文件中没有为指定的 section 和 option 设置 - 值,则返回 default 的值。 - :exception: 不抛出异常 - """ - value = self.config.get(section, option, fallback=default) - if not value: - value = default - return value - - def getint(self, section: str, option: str, default: int = 0) -> int: - """ - 获取指定的 section 和 option 的整数值, - 如果 section 和 option 不存在,则返回 default 的值。 - :exception: - ValueError: 当 default 不为整数,或者 value 不为整数,或者 value 为空时抛出 - """ - if not isinstance(default, int): - msg = "The parameter 'default' must be an integer." - if self.logger: - self.logger.warning(msg) - if self.should_print: - print(msg) - raise ValueError(msg) - try: - value = self.config.getint(section, option, fallback=default) - except ValueError as ex: - msg = "The value must be an integer and can not be empty." - if self.logger: - self.logger.warning(msg) - if self.should_print: - print(msg) - raise ex - return value - - def getfloat(self, section: str, option: str, default: float = 0.0) -> float: - """ - 获取指定的 section 和 option 的浮点数值, - 如果 section 和 option 不存在,则返回 default 的值。 - :exception: - ValueError: 当 default 不为浮点数,或者 value 不为数字,或者 value 为空时抛出 - """ - if not isinstance(default, float): - msg = "The parameter 'default' must be an float." - if self.logger: - self.logger.warning(msg) - if self.should_print: - print(msg) - raise ValueError(msg) - try: - value = self.config.getfloat(section, option, fallback=default) - except ValueError as ex: - msg = "The value must be a number and can not be empty." - if self.logger: - self.logger.warning(msg) - if self.should_print: - print(msg) - raise ex - return value - - def getboolean(self, section: str, option: str, default: bool = False) -> bool: - """ - 获取指定的 section 和 option 的布尔值, - 如果 section 和 option 不存在,则返回 default 的值。 - :exception: - ValueError: 当 default 不为布尔值,或者 value 不为 ('1', '0', 'yes', 'no', 'true', 'false', 'on', 'off') - 其中之一,或者 value 为空时抛出 - """ - if not isinstance(default, bool): - msg = "The parameter 'default' must be an boolean." - if self.logger: - self.logger.warning(msg) - if self.should_print: - print(msg) - raise ValueError(msg) - try: - value = self.config.getboolean(section, option, fallback=default) - except ValueError as ex: - msg = "The specified value must be one of ('1', '0', 'yes', 'no', 'true', 'false', 'on', 'off') " \ - "and cannot be empty." - if self.logger: - self.logger.warning(msg) - if self.should_print: - print(msg) - raise ex - return value - - def get_all_options(self, section: str, default: Union[None, dict] = None) -> dict: - """ - 获取指定的 section 下的所有 options 及其对应的 values。 - 当 section 不存在时,返回默认值。 - :exception: 不抛出异常 - """ - if default is None: - default = {} - try: - items = self.config.items(section) - except configparser.NoSectionError: - msg = f"The section [{section}] does not exists in the configuration file {self.file_path}." - if self.logger: - self.logger.warning(msg) - if self.should_print: - print(msg) - return default - if items: - return dict(items) - return default - - def set(self, section: str, option: str, value: str): - """ - 设置指定的 section 和 option 的 value。 - :exception: - configparser.NoSectionError: 当指定的 section 不存在时抛出 - TypeError: 当 option 和 value 的值不会字符串时抛出 - """ - try: - self.config.set(section, option, value) - except configparser.NoSectionError as ex: - msg = f"The section [{section}] does not exists in the configuration file {self.file_path}." - if self.logger: - self.logger.warning(msg) - if self.should_print: - print(msg) - raise ex - except TypeError as ex: - if self.logger: - self.logger.warning(ex) - if self.should_print: - print(ex) - raise ex - - def save(self): - """ - 将调用 set() 方法产生的改定保存到文件中。 - :exception: - FileError: 当文件校验失败时抛出 - """ - if not self._check_file_permission(os.W_OK): - raise FileError('Failed to check file.') - with open(self.file_path, 'w', encoding="utf-8") as f: - self.config.write(f) diff --git a/oedp-server/utils/file_handler/json_handler.py b/oedp-server/utils/file_handler/json_handler.py deleted file mode 100644 index 26d67f7..0000000 --- a/oedp-server/utils/file_handler/json_handler.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-14 -# ====================================================================================================================== - -import json -import os - -from utils.file_handler.base_handler import BaseHandler, FileError - - -class JSONHandler(BaseHandler): - - def __init__(self, **kwargs): - """ - 初始化 - :param kwargs: - file_path 文件路径 - logger 日志记录器,当该参数不为 None 时,打印日志 - should_print 是否打印提示,当 logger 参数为 None 时才会生效,True 表示打印提示信息 - :exception: - FileError: 当文件校验失败后抛出 - json.JSONDecodeError: 当 JSON 文件格式异常时抛出 - """ - super().__init__(**kwargs) - self.data = None - self._read() - - def _read(self): - """ - 读取 JSON 文件内容 - :exception: - FileError: 当文件校验失败后抛出 - json.JSONDecodeError: 当 JSON 文件格式异常时抛出 - """ - if not (self._check_file_path() and self._check_file_permission()): - raise FileError('Failed to check file.') - try: - with open(self.file_path, 'r') as file: - self.data = json.load(file) - except json.JSONDecodeError as ex: - if self.logger: - self.logger.warning(ex) - if self.should_print: - print(ex) - raise ex - - def save(self): - """ - 保存修改内容 - :exception: - FileError: 当文件校验失败后抛出 - """ - if self._check_file_permission(mode=os.W_OK): - raise FileError('Failed to check file.') - with os.fdopen(os.open( - self.file_path, - os.O_WRONLY | os.O_CREAT | os.O_TRUNC, - 0o640 - ), 'w') as file: - json.dump(self.data, file, indent=2) diff --git a/oedp-server/utils/logger.py b/oedp-server/utils/logger.py deleted file mode 100644 index 72682e9..0000000 --- a/oedp-server/utils/logger.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -import logging -import os -import stat -import sys -import zipfile -from secrets import randbits - -from concurrent_log_handler import ConcurrentRotatingFileHandler - -from constants.paths import LOG_DIR - - -class LogHandler(ConcurrentRotatingFileHandler): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.stream = None - - def do_gzip(self, origin_filename): - if not zipfile: - self._console_log("#no gzip available", stack=False) - return - backup_filename = origin_filename + ".zip" - _, file_name = os.path.split(origin_filename) - with zipfile.ZipFile(backup_filename, 'w', zipfile.ZIP_DEFLATED) as f_handler: - f_handler.write(origin_filename, arcname=file_name) - os.remove(origin_filename) - self._console_log("#gzipped: %s" % (backup_filename,), stack=False) - return - - def doRollover(self): - self._close() - if self.backupCount <= 0: - self.stream = self.do_open("w") - self._close() - return - tmp_name = None - while not tmp_name or os.path.exists(tmp_name): - bit_num = 64 - tmp_name = "%s.rotate.%08d" % (self.baseFilename, randbits(bit_num)) - try: - os.rename(self.baseFilename, tmp_name) - - if self.use_gzip: - self.do_gzip(tmp_name) - except (IOError, OSError): - exc_value = sys.exc_info()[1] - self._console_log("rename failed. File in use? exception=%s" % (exc_value,), stack=True) - return - - gzip_suffix = '' - if self.use_gzip: - gzip_suffix = '.zip' - - def do_rename(source_file, destination_file): - self._console_log("Rename %s -> %s" % (source_file, destination_file + gzip_suffix)) - if os.path.exists(destination_file): - os.remove(destination_file) - if os.path.exists(destination_file + gzip_suffix): - os.remove(destination_file + gzip_suffix) - source_gzip = source_file + gzip_suffix - if os.path.exists(source_gzip): - os.rename(source_gzip, destination_file + gzip_suffix) - elif os.path.exists(source_file): - os.rename(source_file, destination_file) - - for i in range(self.backupCount - 1, 0, -1): - source_file_number = "%s.%d" % (self.baseFilename, i) - dest_file_number = "%s.%d" % (self.baseFilename, i + 1) - if os.path.exists(source_file_number + gzip_suffix): - do_rename(source_file_number, dest_file_number) - dest_file_number = self.baseFilename + ".1" - do_rename(tmp_name, dest_file_number) - - if self.use_gzip: - log_filename = self.baseFilename + ".1.zip" - self._do_chown_and_chmod(log_filename) - - self._console_log("Rotation completed") - - -class AntiCRLFLogRecord(logging.LogRecord): - """ - 记录日志时,转义CRLF, '\n' --> '\\n', '\r' --> '\\r' - """ - - def getMessage(self): - """ - 重写getMessage方法,以转义CRLF - """ - messages = str(self.msg) - if self.args: - messages = messages % self.args - return messages.replace('\n', '\\n').replace('\r', '\\r').replace('\f', '\\f'). \ - replace('\b', '\\b').replace('\v', '\\v').replace('\u007f', '\\u007f') - - -def init_log(file_name): - """ - 初始化logger - """ - if not os.path.isdir(LOG_DIR): - os.makedirs(LOG_DIR) - logging.setLogRecordFactory(AntiCRLFLogRecord) - # 日志文件路径 - log_file = os.path.realpath(os.path.join(LOG_DIR, file_name)) - # 日志文件大小上限 - max_bytes = 10 * 1024 * 1024 - # 日志文件权限 - file_mod = 0o600 - # 日志备份数 - backup_count = 2 - # 日志级别 - log_level = "WARNING" - # 日志输出格式 - log_format = "%(asctime)s|%(process)d:%(thread)d|%(filename)s:%(lineno)d|%(funcName)s|%(levelname)s|%(message)s" - # 日志日期格式 - date_format = '%Y-%m-%d %H:%M:%S %a' - - logger = logging.getLogger(file_name.split(".")[0]) - logger.setLevel(log_level) - handler = LogHandler(log_file, maxBytes=max_bytes, chmod=file_mod, backupCount=backup_count, use_gzip=True) - handler.setFormatter(logging.Formatter(log_format, date_format)) - if not logger.handlers: - logger.addHandler(handler) - - if not os.path.exists(log_file): - os.mknod(log_file) - os.chmod(log_file, stat.S_IREAD + stat.S_IWRITE) - - return logger diff --git a/oedp-server/utils/pagination.py b/oedp-server/utils/pagination.py deleted file mode 100644 index 6b71fdb..0000000 --- a/oedp-server/utils/pagination.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-01-14 -# ====================================================================================================================== - -from rest_framework.pagination import PageNumberPagination -from rest_framework.response import Response - -class CustomPageNumberPagination(PageNumberPagination): - # 每页默认显示的项数 - page_size = 10 - # 每页的最大项目数限制 - max_page_size = 100 - # 通过 URL 参数指定的每页显示项数 - page_size_query_param = 'pageSize' - # 通过 URL 参数指定的当前页数 - page_query_param = 'curPage' - - def get_paginated_response(self, data): - return Response({ - 'links': { - 'next': self.get_next_link(), - 'previous': self.get_previous_link() - }, - 'count': self.page.paginator.count, - 'current_page': self.page.number, - 'total_pages': self.page.paginator.num_pages, - 'results': data - }) diff --git a/oedp-server/utils/ssh/__init__.py b/oedp-server/utils/ssh/__init__.py deleted file mode 100644 index 5ac37c9..0000000 --- a/oedp-server/utils/ssh/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-13 -# ====================================================================================================================== diff --git a/oedp-server/utils/ssh/base_connector.py b/oedp-server/utils/ssh/base_connector.py deleted file mode 100644 index 90debc0..0000000 --- a/oedp-server/utils/ssh/base_connector.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-13 -# ====================================================================================================================== - - -class BaseConnector: - - def __init__(self, ip: str, port: int, username: str, ciphertext_data: dict = None): - self.ip = ip - self.port = port - self.username = username - self.ciphertext_data = ciphertext_data - self.connection = None diff --git a/oedp-server/utils/ssh/ssh_connector.py b/oedp-server/utils/ssh/ssh_connector.py deleted file mode 100644 index 0b1382a..0000000 --- a/oedp-server/utils/ssh/ssh_connector.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-13 -# ====================================================================================================================== - -import pexpect - -from constants.configs.ssh_conf import SSHConfig -from constants.paths import SSH_PROMPTS_JSON_FILE -from utils.cipher import OEDPCipher -from utils.file_handler.json_handler import JSONHandler -from utils.logger import init_log -from utils.ssh.base_connector import BaseConnector - -run_logger = init_log("run.log") - - -class SSHEstablishError(Exception): - - def __init__(self, message): - self._message = message - - def __str__(self): - return self._message - - -class SSHCmdError(Exception): - - def __init__(self, message): - self._message = message - - def __str__(self): - return self._message - - -class SSHCmdTimeoutError(SSHCmdError): - - def __init__(self, message): - super().__init__(message) - - -class SSHConnector(BaseConnector): - # 根据匹配到的提示符判断当前 SSH 连接的状态 - IN_PROGRESS = "In Progress" - SUCCESS = "Success" - FAIL = "Fail" - - # ssh_prompts.json 文件中的密码占位符 - PW_PLACEHOLDER = "PASSWORD" - - # 命令执行结果和命名返回码之间的分隔符 - DELIMITER = "##oedpEMCSL##" - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.expect_prompts = JSONHandler(file_path=SSH_PROMPTS_JSON_FILE, logger=run_logger).data - self._establish() - - def _check_login_status(self, idx, login_prompt_data, child): - prompt_dict = login_prompt_data[idx] - if prompt_dict.get("status") == self.SUCCESS: - run_logger.info("Successfully logged in") - return True - if prompt_dict.get("status") == self.FAIL: - run_logger.error("SSH connection failed, error as follows:") - run_logger.error(f"buffer: {str(child.buffer).strip()}") - run_logger.error(f"before_expect: {str(child.before).strip()}") - run_logger.error(f"after_expect: {str(child.after).strip()}") - raise SSHEstablishError("SSH connection failed") - if prompt_dict.get("status") == self.IN_PROGRESS: - return False - - def _establish(self): - run_logger.info("==== Start to establish SSH connection ====") - - # 创建一个 SSH 子进程 和 child 对象,child 对象用于和子进程交互 - run_logger.info("Start to launch an SSH subprocess") - establish_cmd = f"ssh -o PreferredAuthentications=password -p {self.port} {self.username}@{self.ip}" - try: - child = pexpect.spawn(establish_cmd, encoding='utf-8', timeout=SSHConfig.ESTABLISH_TIMEOUT) - except pexpect.exceptions.ExceptionPexpect as ex: - run_logger.info(f"Failed to establish an SSH connection, error: {str(ex)}") - raise SSHEstablishError(str(ex)) from ex - run_logger.info("Successfully launched SSH subprocess") - - # 登录 - run_logger.info("Start to log in") - login_prompt_data = self.expect_prompts.get("login") - login_prompt_data.append({ - "prompt": pexpect.TIMEOUT, - "send": None, - "status": "Fail", - "message": "SSH connection failed. Error: Timeout or no matching prompt", - "next": None - }) - login_prompt_data.append({ - "prompt": pexpect.EOF, - "send": None, - "status": "Fail", - "message": "SSH connection failed. Error: The SSH connection subprocess has exited", - "next": None - }) - login_prompts = [_.get("prompt") for _ in login_prompt_data if _.get("prompt")] - idx = child.expect(login_prompts, timeout=SSHConfig.ESTABLISH_TIMEOUT) - if self._check_login_status(idx, login_prompt_data, child): - self.connection = child - return - else: - prompt_dict = login_prompt_data[idx] - while True: - send_str = prompt_dict.get("send") - prompt_dict = prompt_dict.get("next") - if send_str == self.PW_PLACEHOLDER: - # 解密密文数据字典,获取明文密码字典 - run_logger.info("Start to decrypt the password") - oedp_oedpcipher = OEDPCipher() - plaintext_dict = oedp_oedpcipher.decrypt_ciphertext_data(self.ciphertext_data) - run_logger.info("successfully decrypted password") - send_str = plaintext_dict.get("password") - del plaintext_dict - child.sendline(send_str) - if prompt_dict is None: - break - idx = child.expect(login_prompts, timeout=SSHConfig.ESTABLISH_TIMEOUT) - if self._check_login_status(idx, login_prompt_data, child): - self.connection = child - - run_logger.info("==== Successfully established SSH connection ====") - - def _set_window_size(self, cmd): - window_height = SSHConfig.WINDOW_HEIGHT - window_width = len(cmd) + SSHConfig.WINDOW_BUFFER_WIDTH - if self.connection: - self.connection.setwinsize(window_height, window_width) - - def execute_cmd(self, cmd, timeout=SSHConfig.EXECUTE_CMD_TIMEOUT): - run_logger.info(f"==== Start to execute command [{cmd}] ====") - - cmd_ = f'res=$({cmd}); echo "$res{self.DELIMITER}$?"' - expect_prompt = f'\r\n.*{self.DELIMITER}[0-9]+' - self._set_window_size(cmd_) - self.connection.sendline(cmd_) - try: - self.connection.expect(expect_prompt, timeout=timeout) - except pexpect.TIMEOUT: - msg = f"Command [{cmd}] timed out" - run_logger.error(msg) - raise SSHCmdTimeoutError(msg) - except pexpect.EOF: - msg = "SSH subprocess exited abnormally during command execution" - run_logger.error(msg) - raise SSHCmdTimeoutError(msg) - res = str(self.connection.after).split(self.DELIMITER) - return_code = res[-1] - std = res[-2].strip('$?"').strip() - if not return_code.isdigit(): - msg = f"The return code ({return_code}) is not a number" - run_logger.error(msg) - raise ValueError(msg) - run_logger.info(f"The return code: {return_code}. The command's output: {std}") - - run_logger.info(f"==== Successfully executed command ====") - return std, return_code diff --git a/oedp-server/utils/test/__init__.py b/oedp-server/utils/test/__init__.py deleted file mode 100644 index 8113de2..0000000 --- a/oedp-server/utils/test/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-03-04 -# ====================================================================================================================== diff --git a/oedp-server/utils/test/testcases.py b/oedp-server/utils/test/testcases.py deleted file mode 100644 index 1a5b43c..0000000 --- a/oedp-server/utils/test/testcases.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-25 -# ====================================================================================================================== - -from rest_framework.test import APITestCase, APIClient - -from usermanager.models import User - - -class CustomTestCase(APITestCase): - - @property - def anonymous_client(self): - if hasattr(self, '_anonymous_client'): - return self._anonymous_client - self._anonymous_client = APIClient() - return self._anonymous_client - - def create_user(self, username, password=None, role=User.Role.ADMIN): - if password is None: - password = "TestPW_123" - return User.objects.create_user(username, password=password, role=role) diff --git a/oedp-server/utils/time.py b/oedp-server/utils/time.py deleted file mode 100644 index 1531957..0000000 --- a/oedp-server/utils/time.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (c) 2025 Huawei Technologies Co., Ltd. -# oeDeploy is licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. -# Create: 2025-02-05 -# ====================================================================================================================== - -import pytz - -from utils.cmd_executor import CommandExecutor - - -def get_time_zone(): - """ - 获取系统时区 - :return: 返回系统时区,若未获取到系统时区则默认返回 Asia/Shanghai - """ - default_time_zone = 'Asia/Shanghai' - cmd = ['ls', '-l', '/etc/localtime'] - stdout, stderr, return_code = CommandExecutor(cmd).run() - - # 如果 return_code 为非 0,则命令执行失败 - if return_code: - return default_time_zone - - contents = stdout.split('zoneinfo/') - # 如果分割后的结果有两部分,说明成功获取时区信息 - if len(contents) == 2: - time_zone = contents[-1].strip() - # 校验获取的时区是否合法 - try: - pytz.timezone(time_zone) - except pytz.UnknownTimeZoneError: - return default_time_zone # 如果非法,返回默认时区 - return time_zone # 如果合法,返回获取的时区 - return default_time_zone diff --git a/oedp-ui/.keep b/oedp-ui/.keep deleted file mode 100644 index e69de29..0000000 -- Gitee