chore: 整理项目结构,清理归档文件,更新部署配置

This commit is contained in:
Simon
2026-07-04 21:01:39 +08:00
parent 8bd4ab0366
commit 64ff1bf7d5
508 changed files with 43575 additions and 14129 deletions
+346
View File
@@ -0,0 +1,346 @@
"""
JumpServer elFinder 自动化上传脚本
通过 Playwright 浏览器自动化,登录 JumpServer,
打开目标资产的 elFinder 文件管理器,上传部署包。
用法:
python jumpserver_elfinder_upload.py --file deploy-server/it-smart-desk-server-deploy.zip --remote /tmp/
python jumpserver_elfinder_upload.py -f deploy-server/it-smart-desk-server-deploy.zip -r /tmp/ --headless
"""
import sys, os, json, base64, time, pyotp, argparse
from pathlib import Path
# 路径配置
SCRIPT_DIR = Path(__file__).parent
PROJECT_ROOT = SCRIPT_DIR.parent
WORKBUDDY_DIR = Path(os.environ.get("WORKBUDDY_DIR", Path.home() / ".workbuddy"))
SKILL_DIR = Path(os.environ.get("JP_SKILL_DIR", WORKBUDDY_DIR / "skills" / "jumpserver-automation"))
CONFIG_PATH = SKILL_DIR / "config" / "jumpserver_config.json"
OTP_SECRET_PATH = SKILL_DIR / "scripts" / "otp_secret.key"
OUTPUT_DIR = SKILL_DIR / "scripts" / "webcli_output"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
TARGET_ASSET_NAME = "hz-oa-ai-g-dataquery-90-5-110"
TARGET_IP = "10.90.5.110"
def load_config():
"""加载 JumpServer 配置(用户名 + Base64密码解码)"""
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
config = json.load(f)
password = base64.b64decode(config["password"]).decode("utf-8")
return config["url"].rstrip("/"), config.get("username", "sxn"), password
def load_otp_secret():
"""加载 TOTP 密钥"""
if OTP_SECRET_PATH.exists():
return OTP_SECRET_PATH.read_text(encoding="utf-8").strip()
return None
def screenshot(page, name):
"""截图到输出目录"""
path = OUTPUT_DIR / name
page.screenshot(path=str(path))
print(f" 📸 {name}")
return str(path)
def upload_file(local_file_path, remote_dir="/tmp/", headless=False):
"""
通过 JumpServer elFinder Web 界面上传文件到远程服务器
流程:
1. Playwright 浏览器打开 → JumpServer 登录页
2. 用户名 + 密码登录
3. TOTP MFA 验证
4. 进入「我的资产」列表
5. 找到目标主机 → 点击「文件管理」按钮
6. 在新标签页加载 elFinder 文件管理器
7. 导航到目标目录
8. 通过文件上传 input 上传文件
9. 验证上传成功
"""
local_file = Path(local_file_path)
if not local_file.exists():
print(f"❌ 本地文件不存在: {local_file_path}")
return False
file_size_mb = local_file.stat().st_size / (1024 * 1024)
print(f"📦 本地文件: {local_file_path} ({file_size_mb:.2f} MB)")
print(f"📂 目标目录: {remote_dir}")
url, username, password = load_config()
otp_secret = load_otp_secret()
if not otp_secret:
print("❌ 找不到 otp_secret.key")
return False
otp_code = pyotp.TOTP(otp_secret).now()
print(f"🔐 OTP: {otp_code}")
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# 启动 Chrome 浏览器
print("🚀 启动浏览器...")
try:
browser = p.chromium.launch(
channel="chrome",
headless=headless,
args=["--no-sandbox", "--disable-dev-shm-usage"]
)
except Exception:
browser = p.chromium.launch(headless=headless)
context = browser.new_context(
viewport={"width": 1280, "height": 900},
ignore_https_errors=True
)
page = context.new_page()
try:
# ========== 步骤 1: 登录 ==========
print("\n[1/7] 登录 JumpServer...")
page.goto(f"{url}/users/login/", wait_until="networkidle", timeout=30000)
page.wait_for_timeout(1500)
page.fill('input[name="username"]', username, timeout=5000)
page.fill('input[type="password"]', password, timeout=5000)
page.click('button[type="submit"]', timeout=5000)
print(" ✅ 已登录")
# ========== 步骤 2: MFA 验证 ==========
print("[2/7] MFA 验证...")
page.wait_for_timeout(3000)
content = page.content()
if any(kw in content for kw in ["mfa_type", "MFA", "多因子", "验证码"]):
print(f" 填写 OTP: {otp_code}")
try:
sel = page.locator('select[name="mfa_type"], select#mfa-select')
if sel.count() > 0 and sel.first.input_value() != 'otp':
sel.first.select_option('otp')
except:
pass
code_in = page.locator('input[name="code"]')
if code_in.count() > 0:
code_in.fill(str(otp_code), timeout=5000)
page.locator('#submit_button, button[type="submit"]').first.click(timeout=5000)
print(" ✅ OTP 提交成功")
# ========== 步骤 3: 进入资产列表 ==========
print("[3/7] 进入「我的资产」...")
page.wait_for_timeout(4000)
page.get_by_text("我的资产").first.click(timeout=10000)
page.wait_for_timeout(2000)
screenshot(page, "upload_asset_list.png")
print(f" 📍 URL: {page.url}")
# ========== 步骤 4: 进入文件管理页面 ==========
print(f"[4/7] 进入「文件管理」...")
# 文件管理是左侧菜单项,不是资产行按钮!
fm_menu = page.get_by_text("文件管理").first
if fm_menu.count() > 0:
fm_menu.click(timeout=10000)
page.wait_for_timeout(3000)
print(f" ✅ 点击了左侧菜单「文件管理」")
screenshot(page, "upload_file_manager_page.png")
print(f" 📍 URL: {page.url}")
else:
print(" ❌ 未找到左侧菜单「文件管理」")
# 备用方案:直接访问 elFinder URL
elfinder_url = f"{url}/koko/elfinder/?target={TARGET_ASSET_NAME}&search="
page.goto(elfinder_url, wait_until="domcontentloaded", timeout=15000)
page.wait_for_timeout(3000)
print(f" ✅ 直接打开 elFinder: {elfinder_url}")
# 在文件管理页面中选择目标资产(如果需要)
page.wait_for_timeout(2000)
page.wait_for_timeout(4000)
screenshot(page, "upload_after_fm_click.png")
# ========== 步骤 5: 等待 elFinder 加载 ==========
print("[5/7] 加载 elFinder 文件管理器...")
# 文件管理页面可能直接在当前页面加载 elFinder
elfinder_page = page
page.wait_for_timeout(5000)
screenshot(elfinder_page, "upload_elfinder_loaded.png")
# ========== 步骤 6: 导航到目标目录 ==========
print(f"[6/7] 导航到目标目录: {remote_dir}")
# elFinder 文件树导航
# 尝试通过 JS 直接操作 elFinder 实例
try:
result = elfinder_page.evaluate(f"""
() => {{
try {{
const elfinder = window.elfFinder ||
(window.jQuery && window.jQuery('#elfinder').data('elfinder'));
if (elfinder) {{
elfinder.exec('open', '{remote_dir}');
return 'navigating to {remote_dir}';
}}
return 'elfinder instance not found';
}} catch(e) {{
return 'JS error: ' + e.message;
}}
}}
""")
print(f" 📂 导航结果: {result}")
except Exception as e:
print(f" ⚠️ JS 导航异常: {e}")
try:
elfinder_page.keyboard.press("Control+l")
elfinder_page.wait_for_timeout(500)
elfinder_page.keyboard.type(remote_dir)
elfinder_page.keyboard.press("Enter")
elfinder_page.wait_for_timeout(2000)
print(" 📂 键盘导航到目标目录")
except Exception as e2:
print(f" ⚠️ 键盘导航也失败了: {e2}")
page.wait_for_timeout(2000)
screenshot(elfinder_page, "upload_before_upload.png")
# ========== 步骤 7: 上传文件 ==========
print("[7/7] 上传文件...")
uploaded = False
upload_selectors = [
'input[type="file"][name="upload[]"]',
'input[type="file"].elfinder-upload-file',
'.elfinder-upload-dialog input[type="file"]',
'#elfinder input[type="file"]',
'.elfinder input[type="file"]',
'input[type="file"]'
]
upload_input = None
for sel in upload_selectors:
try:
el = elfinder_page.locator(sel).first
if el.count() > 0:
upload_input = el
print(f" ✅ 找到上传控件: {sel}")
break
except:
continue
if upload_input:
try:
upload_btn = elfinder_page.locator(
'.elfinder-toolbar-button:has-text("上传"), '
'.elfinder-buttonset .ui-button:has(.ui-icon-upload), '
'button:has-text("Upload"), button:has-text("上传")'
).first
if upload_btn.count() > 0:
upload_btn.click(timeout=3000)
elfinder_page.wait_for_timeout(1000)
print(" 📤 点击了上传按钮")
abs_path = str(local_file.absolute())
upload_input.set_input_files(abs_path, timeout=10000)
print(f" ✅ 文件已选择: {abs_path}")
wait_time = max(60, int(file_size_mb * 20))
print(f" ⏳ 等待上传完成(预计 {wait_time} 秒)...")
for i in range(wait_time // 5):
elfinder_page.wait_for_timeout(5000)
try:
progress_hidden = elfinder_page.evaluate("""
() => {
const pbar = document.querySelector('.elfinder-progressbar, .elfinder-upload-progress');
return !pbar || pbar.style.display === 'none' || pbar.offsetParent === null;
}
""")
if progress_hidden:
print(f" ✅ 上传进度条已消失,上传可能完成")
break
except:
pass
if (i + 1) % 5 == 0:
print(f" ⏳ 仍在上传... ({i * 5}s)")
elfinder_page.wait_for_timeout(3000)
screenshot(elfinder_page, "upload_complete.png")
try:
file_check = elfinder_page.evaluate(f"""
() => {{
try {{
const fm = window.elfFinder ||
(window.jQuery && window.jQuery('#elfinder').data('elfinder'));
if (fm) {{
const files = fm.cwd().files;
for (const f of files) {{
if (f.name === '{local_file.name}') {{
return JSON.stringify({{name: f.name, size: f.size, found: true}});
}}
}}
}}
return '{{"found": false}}';
}} catch(e) {{
return '{{"error": "' + e.message + '"}}';
}}
}}
""")
print(f" 📋 文件验证: {file_check}")
except Exception as e:
print(f" ⚠️ 无法验证文件: {e}")
uploaded = True
print(" ✅ 上传完成!")
except Exception as e:
print(f" ❌ 上传失败: {e}")
try:
screenshot(elfinder_page, "upload_error.png")
except:
pass
else:
print(" ❌ 未找到上传控件,尝试 file chooser...")
try:
with elfinder_page.expect_file_chooser() as fc_info:
elfinder_page.locator('body').click(position={"x": 100, "y": 100})
elfinder_page.wait_for_timeout(500)
file_chooser = fc_info.value
file_chooser.set_files(str(local_file.absolute()))
elfinder_page.wait_for_timeout(3000)
uploaded = True
print(" ✅ 通过 file chooser 上传成功!")
except Exception as e:
print(f" ❌ 上传失败: {e}")
if uploaded:
print("\n🎉 上传流程完成!")
return True
else:
print("\n❌ 上传未能完成,请检查截图")
return False
except Exception as e:
print(f"\n💥 异常: {e}")
try:
screenshot(page, "upload_exception.png")
except:
pass
return False
finally:
print("\n📸 截图保存在:", OUTPUT_DIR)
browser.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="JumpServer elFinder 文件上传工具")
parser.add_argument("-f", "--file", required=True, help="本地文件路径")
parser.add_argument("-r", "--remote", default="/tmp/", help="远程目标目录 (默认: /tmp/)")
parser.add_argument("--headless", action="store_true", help="无头模式")
args = parser.parse_args()
success = upload_file(args.file, args.remote, args.headless)
sys.exit(0 if success else 1)