From 491db50f95612c7c994410288b0631c296059ba7 Mon Sep 17 00:00:00 2001 From: Ching L Date: Fri, 12 Dec 2025 18:05:53 +0800 Subject: [PATCH] feat(sso): add interactive SSO management script tools - Added run_sso_script.sh for remote script execution with SSH - Added sso_script.py with Django-based SSO management functions - Implemented XDP Ultra redeem code generation functionality - Added batch product binding and update capabilities - Included comprehensive documentation and usage examples - Added automatic cleanup mechanisms for remote temporary files --- sso/README.md | 120 ++++++++++++++++++ sso/run_sso_script.sh | 89 +++++++++++++ sso/sso_script.py | 288 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 497 insertions(+) create mode 100644 sso/README.md create mode 100755 sso/run_sso_script.sh create mode 100644 sso/sso_script.py diff --git a/sso/README.md b/sso/README.md new file mode 100644 index 0000000..a1a1654 --- /dev/null +++ b/sso/README.md @@ -0,0 +1,120 @@ +# SSO 交互式脚本工具 + +这是一个用于远程执行 SSO (Single Sign-On) 相关管理任务的交互式脚本工具集。 + +## 文件说明 + +- `run_sso_script.sh` - 主要执行脚本,负责将 Python 脚本上传到远程服务器并执行 +- `sso_script.py` - Django 交互式管理脚本,提供 SSO 系统的各种管理功能 + +## 功能概述 + +### run_sso_script.sh 功能 +- 自动将本地 Python 脚本上传到远程 Proxmox 容器 +- 建立交互式 SSH 连接执行脚本 +- 自动清理远程临时文件 +- 支持脚本执行过程中的用户交互 + +### sso_script.py 功能 +该脚本提供以下三个主要管理功能: + +1. **创建 XDP Ultra Redeem Code** + - 生成指定数量的兑换码 + - 用于 XDP Ultra 产品的许可证激活 + +2. **批次绑定商品** + - 将序列号批次与商品进行绑定 + - 支持批量操作以提高效率 + +3. **批次更新商品** + - 更新已绑定序列号的商品信息 + - 支持批量修改商品绑定关系 + +## 使用方法 + +### 环境配置 +在使用前需要确保: + +1. **远程服务器配置**(在 `run_sso_script.sh` 中配置): + ```bash + REMOTE_USER="root" + REMOTE_SERVER="172.24.9.97" + ``` + +2. **本地环境**: + - 确保可以通过 SSH 连接到远程服务器 + - 远程服务器上存在包含 "celery" 名称的 Proxmox 容器 + +### 执行步骤 + +1. **运行主脚本**: + ```bash + ./run_sso_script.sh + ``` + +2. **选择功能**: + 脚本启动后会显示交互式菜单: + ``` + ================================================== + SSO 交互式脚本 + ================================================== + 请选择要执行的功能: + 1. 创建 XDP Ultra Redeem Code + 2. 批次绑定商品 + 3. 批次更新商品 + 0. 退出 + ================================================== + ``` + +3. **按提示输入参数**: + - **功能1**:输入要生成的兑换码数量 + - **功能2**:输入商品ID、批次ID、开始流水号、结束流水号 + - **功能3**:输入新商品ID、批次ID、开始流水号、结束流水号 + +### 使用示例 + +#### 示例1:创建 50 个 XDP Ultra Redeem Code +``` +请选择要执行的功能: 1 +请输入要生成的数量: 50 +``` + +#### 示例2:批次绑定商品 +``` +请选择要执行的功能: 2 +请输入商品ID (product_id): 12345 +请输入批次ID (batch_id): 67890 +请输入开始流水号 (start): 1001 +请输入结束流水号 (stop): 1100 +``` + +#### 示例3:批次更新商品 +``` +请选择要执行的功能: 3 +请输入新商品ID (new_product_id): 54321 +请输入批次ID (batch_id): 67890 +请输入开始流水号 (start): 1001 +请输入结束流水号 (stop): 1100 +``` + +## 技术架构 + +- **远程执行**:使用 SSH 和 Proxmox 容器技术 +- **Django 集成**:脚本运行在 Django 环境中,可访问相关数据模型 +- **数据处理**:使用 MongoDB 和 FlexEngine 进行数据操作 +- **许可证管理**:集成许可证颁发和管理系统 + +## 注意事项 + +1. **权限要求**:需要远程服务器的 root 权限 +2. **网络连接**:确保与远程服务器的网络连通性 +3. **数据安全**:操作涉及生产数据,请谨慎使用 +4. **错误处理**:脚本包含基本错误处理,如遇问题请检查输入参数 +5. **清理机制**:脚本会自动清理远程临时文件,无需手动操作 + +## 故障排除 + +- **连接失败**:检查 SSH 配置和网络连接 +- **容器未找到**:确认远程服务器上存在包含 "celery" 名称的容器 +- **权限错误**:确认用户具有必要的系统权限 +- **参数错误**:检查输入的 ID 和数值是否正确 \ No newline at end of file diff --git a/sso/run_sso_script.sh b/sso/run_sso_script.sh new file mode 100755 index 0000000..961d6ec --- /dev/null +++ b/sso/run_sso_script.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +# --- 配置区 --- +REMOTE_USER="root" +REMOTE_SERVER="172.24.9.97" +PYTHON_SCRIPT_FILE="./sso_script.py" +# --- 配置区结束 --- + +# 检查本地脚本文件是否存在 +if [ ! -f "$PYTHON_SCRIPT_FILE" ]; then + echo "错误: Python脚本文件 $PYTHON_SCRIPT_FILE 不存在!" + exit 1 +fi + +echo "第一步: 正在将脚本上传到远程服务器..." + +# 在远程创建一个唯一的临时文件名 +# 使用 mktemp 在本地生成一个有意义的名字,方便后续引用 +# 格式: /tmp/interactive_script_随机数.py +TEMP_SCRIPT_NAME_ON_REMOTE="/srv/sso/cgi-bin/interactive_script_$$.py" + +# 使用第一个SSH连接,将本地脚本内容写入远程的临时文件 +# 这个连接不需要 -t,因为它只负责传输数据 +# 注意:我们使用 `printf` 来构建远程命令,以避免复杂的引号问题 +ssh "$REMOTE_USER@$REMOTE_SERVER" " + CT_ID=\$(pct list | awk '/celery/ {print \$1; exit}'); + if [ -z \"\$CT_ID\" ]; then + echo '错误: 未找到名称包含 celery 的容器' >&2; + exit 1; + fi; + echo \"找到容器 ID: \$CT_ID\" >&2; + pct exec \"\$CT_ID\" -- bash -c \"cat > $TEMP_SCRIPT_NAME_ON_REMOTE\" +" < "$PYTHON_SCRIPT_FILE" + +# 检查上传是否成功 +if [ $? -ne 0 ]; then + echo "错误: 脚本上传失败。" + exit 1 +fi + +echo "上传成功: $TEMP_SCRIPT_NAME_ON_REMOTE" + +# 设置本地清理机制,确保即使脚本被中断,也能清理远程临时文件 +cleanup_remote() { + echo "正在清理远程临时文件..." >&2 + ssh "$REMOTE_USER@$REMOTE_SERVER" " + CT_ID=\$(pct list | awk '/celery/ {print \$1; exit}'); + if [ -n \"\$CT_ID\" ]; then + pct exec \"\$CT_ID\" -- bash -c \"rm -f $TEMP_SCRIPT_NAME_ON_REMOTE\" 2>/dev/null || true; + fi + " 2>/dev/null || true +} +trap cleanup_remote EXIT INT TERM + +echo "----------------------------------------------------" +echo "第二步: 正在连接到远程服务器以交互式执行脚本..." +echo "(执行完毕后,临时文件将被自动删除)" +echo "----------------------------------------------------" + +# 使用第二个SSH连接,这个连接是纯粹的交互式连接 (-t) +# 它负责执行远程的脚本,并连接你的终端 +# 注意:我们使用 `pct exec` 而不是 `pct enter`,这样更脚本化 +ssh -t "$REMOTE_USER@$REMOTE_SERVER" " + CT_ID=\$(pct list | awk '/celery/ {print \$1; exit}'); + if [ -z \"\$CT_ID\" ]; then + echo '错误: 未找到名称包含 celery 的容器' >&2; + exit 1; + fi; + # 定义一个退出清理函数 + cleanup() { + echo \"清理远程临时文件...\"; + pct exec \"\$CT_ID\" -- bash -c \"rm -f $TEMP_SCRIPT_NAME_ON_REMOTE\"; + echo \"清理完成。\"; + } + # 设置脚本结束时(无论是正常退出还是被中断)都执行清理 + trap cleanup EXIT + + # 执行脚本(使用 set -e 确保任何命令失败都会退出) + pct exec \"\$CT_ID\" -- bash -c \" + set -e; + cd /srv/sso/cgi-bin/; + source /srv/venv/bin/activate; + python $TEMP_SCRIPT_NAME_ON_REMOTE + \" || { echo '错误: Python 脚本执行失败' >&2; exit 1; }; +" + +echo "----------------------------------------------------" +echo "远程交互式脚本已结束。" + diff --git a/sso/sso_script.py b/sso/sso_script.py new file mode 100644 index 0000000..2209e6b --- /dev/null +++ b/sso/sso_script.py @@ -0,0 +1,288 @@ +import os +import sys +import django + +# --- 关键的 Django 设置 --- +project_path = '/srv/sso' +if project_path not in sys.path: + sys.path.append(project_path) +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sso.settings') +django.setup() + +import canton.models +from canton.models import * +import bson +import uuid +from utils import const +import hydrogen.flex_engine +import requests +from django.utils.timezone import get_current_timezone, localtime, now +import datetime +import utils.const +import django.conf + + +# --- 功能函数定义 --- +def get_engine(table_name): + return hydrogen.flex_engine.FlexEngine.get_engine_by_name( + miniapp_id=44328, + schema_name=table_name, + enable_acl_check=False) + + +def gen_code(info_id, count, print_only=False): + info_engine = get_engine('license_info') + code_engine = get_engine('license_redeem_code') + + info = info_engine.get_by_id(info_id) + + uuids = [] + for x in range(count): + uuids.append(str(uuid.uuid4())) + + for uid in uuids: + code_engine.create( + code=uid, + status='valid', + valid_from=info['valid_from'], + valid_until=info['valid_until'], + license_info=bson.dbref.DBRef( + info_engine.collection.name, + info['_id']), + created_by=27068886, + _read_perm=[const.HYDROGEN_ACL_CREATED_BY_TPL], + _write_perm=[], + ) + if print_only: + print(uid) + if print_only: + return + # save uuids to csv + fn = '/tmp/%s-%s.csv' % (info['name'].replace(' ', ''), localtime().strftime('%Y%m%d')) + with open(fn, 'a') as f: + for uid in uuids: + f.write(uid + '\n') + + print(fn) + + +def issue_license(code, psn): + code_engine = get_engine('license_redeem_code') + code = code_engine.filter(code=code, status=utils.const.TANGZHI_LICENSE_REDEEM_CODE_STATUS_VALID)[0] + if not code: + return False + code_engine = get_engine('license_redeem_code') + license_info = code_engine.dereference(code['license_info']) + feature_engine = get_engine('license_feature') + license_data = { + 'license': { + 'psn': psn, + 'features': [], + 'signing_key': 'IFANR_IOT_KEY', + }, + 'enable_cloud_push': True} + for feature_id in license_info['license_feature']: + feature = feature_engine.get(feature_id=feature_id) + if feature and 'CP02' in feature['applicable_devices']: + license_data['license']['features'].append({ + 'feature_id': feature_id, + 'applicable_devices': feature['applicable_devices'], + }) + if not license_data['license']['features']: + return + if license_info['license_type'] == utils.const.TANGZHI_LICENSE_TYPE_TIMED: + if license_info.get('expiry_days'): + license_data['license']['not_before'] = utils.timestamp_of(now()) + license_data['license']['not_after'] = utils.timestamp_of( + now() + datetime.timedelta( + days=license_info['expiry_days'])) + else: + license_data['license']['not_before'] = license_info['not_before'] + license_data['license']['not_after'] = license_info['not_after'] + elif license_info['license_type'] == utils.const.TANGZHI_LICENSE_TYPE_COUNTER: + license_data['license']['max_usage'] = license_info['max_usage'] + elif license_info['license_type'] == utils.const.TANGZHI_LICENSE_TYPE_RELATIVE_TIME: + license_data['license']['validity_hours'] = license_info['validity_hours'] + + resp = requests.post( + django.conf.settings.TANGZHI_LICENSE_ISSUE_URL, + json=license_data) + if resp.status_code != utils.const.HTTP_OK: + print('Failed to issue license: %s, data: %s' % (resp.text, license_data)) + return + device_license_engine = get_engine( + django.conf.settings.IOT_DEVICE_LICENSE_SCHEMA_NAME) + snapshot = { + 'license_redeem_code': code_engine.document_2_dict(code, dereference=True) + } + code_engine.update_by_id( + code['_id'], + psn=psn) + device_license_engine.update_by_id( + resp.json()['license']['id'], + snapshot=snapshot) + + resp_data = device_license_engine.get_by_id( + resp.json()['license']['id']) + for feature in resp_data['license']['features']: + feature['tags'] = feature_engine.get(feature_id=feature['feature_id'])['tags'] + + return resp_data + + +def bind_product(product_id, batch_id, start, stop): + """绑定商品到序列号批次""" + # 绑定商品 + null_product = canton.models.ProductInfo.objects.filter(name='').first() + new_product = canton.models.ProductInfo.objects.filter(id=product_id).first() + + psn__in = [_data['psn'] for _data in canton.models.SerialNumber.generate_serial_number(**{ + 'batch_id': batch_id, + 'start': start, + 'stop': stop, + 'sign': False, + })] + + sns = SerialNumber.objects.filter(psn__in=psn__in) + + fail_sns = sns.filter(~Q(product=null_product) & ~Q(product=new_product)) + if fail_sns: + raise ValueError("部分设备已绑定 %s" % fail_sns.values_list('psn', flat=True)) + + sns.update(product=new_product) + return sns.last().serialize() + + +def update_product(new_product_id, batch_id, start, stop): + """更新商品绑定""" + # 更新商品 + new_product = canton.models.ProductInfo.objects.filter(id=new_product_id).first() + + psn__in = [_data['psn'] for _data in canton.models.SerialNumber.generate_serial_number(**{ + 'batch_id': batch_id, + 'start': start, + 'stop': stop, + 'sign': False, + })] + + sns = SerialNumber.objects.filter(psn__in=psn__in) + + fail_sns = sns.filter(product=new_product) + if fail_sns: + raise ValueError("部分设备已绑定为当前商品 %s" % fail_sns.values_list('psn', flat=True)) + + sns.update(product=new_product) + return sns.last().serialize() + + +# --- 交互式菜单功能 --- +def create_xdp_ultra_redeem_code(): + """创建 xdp ultra redeem code""" + print("\n=== 创建 XDP Ultra Redeem Code ===") + count = input("请输入要生成的数量: ") + try: + count = int(count) + if count <= 0: + print("数量必须大于 0") + return + print(f"\n开始生成 {count} 个 redeem code...") + # ultra 购买 + info_id = '677758f12c450457fad77408' + gen_code(info_id, count, True) + print(f"\n成功生成 {count} 个 redeem code") + except ValueError: + print("错误: 请输入有效的数字") + except Exception as e: + print(f"执行出错: {e}") + + +def batch_bind_product(): + """批次绑定商品""" + print("\n=== 批次绑定商品 ===") + try: + product_id = int(input("请输入商品ID (product_id): ")) + batch_id = int(input("请输入批次ID (batch_id): ")) + start = int(input("请输入开始流水号 (start): ")) + stop = int(input("请输入结束流水号 (stop): ")) + + print(f"\n开始绑定商品...") + print(f"商品ID: {product_id}, 批次ID: {batch_id}, Start: {start}, Stop: {stop}") + + result = bind_product(product_id, batch_id, start, stop) + print(f"\n绑定成功!") + print(f"结果: {result}") + except ValueError as e: + print(f"输入错误: {e}") + except Exception as e: + print(f"执行出错: {e}") + + +def batch_update_product(): + """批次更新商品""" + print("\n=== 批次更新商品 ===") + try: + new_product_id = int(input("请输入新商品ID (new_product_id): ")) + batch_id = int(input("请输入批次ID (batch_id): ")) + start = int(input("请输入开始流水号 (start): ")) + stop = int(input("请输入结束流水号 (stop): ")) + + print(f"\n开始更新商品...") + print(f"新商品ID: {new_product_id}, 批次ID: {batch_id}, Start: {start}, Stop: {stop}") + + result = update_product(new_product_id, batch_id, start, stop) + print(f"\n更新成功!") + print(f"结果: {result}") + except ValueError as e: + print(f"输入错误: {e}") + except Exception as e: + print(f"执行出错: {e}") + + +def show_menu(): + """显示功能菜单""" + print("\n" + "="*50) + print("SSO 交互式脚本") + print("="*50) + print("请选择要执行的功能:") + print("1. 创建 XDP Ultra Redeem Code") + print("2. 批次绑定商品") + print("3. 批次更新商品") + print("0. 退出") + print("="*50) + + +def main(): + """主函数""" + print("--- 开始执行交互式 Django 脚本 ---\n") + + while True: + show_menu() + choice = input("\n请输入功能编号: ").strip() + + if choice == '1': + create_xdp_ultra_redeem_code() + elif choice == '2': + batch_bind_product() + elif choice == '3': + batch_update_product() + elif choice == '0': + print("\n退出脚本...") + break + else: + print("\n无效的选择,请重新输入") + + # 询问是否继续 + if choice != '0': + continue_choice = input("\n是否继续执行其他功能? (y/n): ").strip().lower() + if continue_choice != 'y': + print("\n退出脚本...") + break + + print("\n--- 脚本执行完毕 ---") + + +# 执行主函数 +if __name__ == '__main__': + main() + +