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
This commit is contained in:
parent
db8f25a518
commit
491db50f95
120
sso/README.md
Normal file
120
sso/README.md
Normal file
@ -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 和数值是否正确
|
||||
89
sso/run_sso_script.sh
Executable file
89
sso/run_sso_script.sh
Executable file
@ -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 "远程交互式脚本已结束。"
|
||||
|
||||
288
sso/sso_script.py
Normal file
288
sso/sso_script.py
Normal file
@ -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()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user