216 lines
8.9 KiB
Python
216 lines
8.9 KiB
Python
import time
|
||
from framework.core.base_page import BasePage
|
||
from framework.core.logger import get_logger
|
||
|
||
logger = get_logger("DevMachinePage")
|
||
|
||
class DevMachinePage(BasePage):
|
||
"""开发机页面 POM - 纯原子操作层"""
|
||
|
||
MENU_TEXT = "开发机"
|
||
URL_FRAGMENT = "/dev-machine"
|
||
|
||
def __init__(self, page):
|
||
self.page = page
|
||
|
||
def navigate_to(self):
|
||
"""进入开发机管理页面 (含强力跳转)"""
|
||
logger.info("正在切换到【开发机】页面...")
|
||
if self.URL_FRAGMENT in self.page.url:
|
||
return
|
||
|
||
for i in range(3):
|
||
self.page.evaluate("document.querySelectorAll('.p-dialog-mask').forEach(el => el.style.display='none')")
|
||
self.page.evaluate(f"""() => {{
|
||
const target = Array.from(document.querySelectorAll('.p-menuitem-link, a, span'))
|
||
.find(el => el.innerText.trim() === '{self.MENU_TEXT}');
|
||
if (target) target.click();
|
||
}}""")
|
||
time.sleep(3)
|
||
if self.URL_FRAGMENT in self.page.url:
|
||
logger.info("✅ 成功进入开发机页面")
|
||
return
|
||
|
||
# 强制重定向兜底
|
||
if self.URL_FRAGMENT not in self.page.url:
|
||
target_url = self.page.url.split('#')[0].split('?')[0].replace('/file-manager', self.URL_FRAGMENT)
|
||
self.page.goto(target_url)
|
||
time.sleep(3)
|
||
|
||
def get_first_machine_status(self):
|
||
"""查询首台实例状态"""
|
||
try:
|
||
self.page.wait_for_selector("tr", timeout=5000)
|
||
status = self.page.evaluate("""() => {
|
||
const rows = document.querySelectorAll('tr');
|
||
if (rows.length < 2) return 'None';
|
||
const cells = rows[1].querySelectorAll('td');
|
||
return cells.length > 2 ? cells[2].innerText.trim() : 'Unknown';
|
||
}""")
|
||
logger.info(f"✅ 探测到首台开发机状态: {status}")
|
||
return status
|
||
except:
|
||
return "Empty"
|
||
|
||
def open_apply_dialog(self):
|
||
"""点击申请按钮"""
|
||
logger.info("👉 点击 [申请开发机] 按钮")
|
||
self.smart_click("申请开发机")
|
||
time.sleep(1)
|
||
|
||
def fill_name(self, name):
|
||
"""填写开发机名称"""
|
||
logger.info(f"⌨️ 正在输入名称: {name}")
|
||
self.smart_fill("名称", name)
|
||
|
||
def select_sku(self, sku_id):
|
||
"""选择配置 SKU"""
|
||
logger.info(f"🎯 尝试选择 SKU: {sku_id}")
|
||
self.page.evaluate(f"""(id) => {{
|
||
const input = document.getElementById(id + '-input') || document.getElementById(id);
|
||
if (input) {{ input.click(); return true; }}
|
||
return false;
|
||
}}""", sku_id)
|
||
|
||
def select_image(self, image_keyword="CUDA"):
|
||
"""选择镜像"""
|
||
logger.info(f"💿 正在选择镜像: {image_keyword}")
|
||
self.smart_click("选择镜像")
|
||
time.sleep(1)
|
||
self.smart_click(image_keyword)
|
||
time.sleep(1)
|
||
self.smart_click("确定", role="button")
|
||
time.sleep(1)
|
||
|
||
def fill_ssh_key(self, ssh_key):
|
||
"""填写 SSH 公钥 (使用增强加固后的智能填写)"""
|
||
logger.info(f"⌨️ 正在输入 SSH 公钥")
|
||
self.smart_fill("SSH公钥", ssh_key)
|
||
|
||
def fill_system_disk(self, size="100"):
|
||
"""填写系统盘大小"""
|
||
logger.info(f"⌨️ 正在输入系统盘大小: {size}")
|
||
self.smart_fill("请输入系统盘大小", size)
|
||
|
||
def submit_application(self):
|
||
"""点击申请创建按钮"""
|
||
logger.info("🚀 提交申请创建")
|
||
self.smart_click("申请创建")
|
||
time.sleep(2)
|
||
|
||
def get_machine_status(self, machine_name):
|
||
"""查找指定名称机器的真实行文字 (精准锚点)"""
|
||
return self.page.evaluate(f"""(name) => {{
|
||
// 优先查找表格行
|
||
const rows = Array.from(document.querySelectorAll('tr'));
|
||
const dataRow = rows.find(r => {{
|
||
const text = r.innerText;
|
||
// 必须包含名字,且为了避免误中面包屑,行内字符数应该较多
|
||
return text.includes(name) && text.trim().length > name.length + 5;
|
||
}});
|
||
|
||
if (dataRow) return dataRow.innerText.trim();
|
||
|
||
// 如果没找到表格行,尝试找包含该名字的最近的一个容器类元素
|
||
const anyRow = Array.from(document.querySelectorAll('.p-datatable-row, .p-selectable-row'))
|
||
.find(r => r.innerText.includes(name));
|
||
|
||
return anyRow ? anyRow.innerText.trim() : 'Not Found';
|
||
}}""", machine_name)
|
||
|
||
def wait_for_status(self, name, expected_status, timeout=600):
|
||
"""带心跳的状态等待器 (增强版)"""
|
||
logger.info(f"⏳ 等待开发机 {name} 状态变为: {expected_status}...")
|
||
start = time.time()
|
||
last_log = 0
|
||
while time.time() - start < timeout:
|
||
current = self.get_machine_status(name)
|
||
|
||
# 命中状态
|
||
if expected_status in current:
|
||
logger.info(f"✅ 状态达标: {current}")
|
||
return True
|
||
|
||
# 心跳日志
|
||
if time.time() - last_log > 10:
|
||
# 截取前 50 个字符避免日志过长
|
||
snippet = (current[:50] + '...') if len(current) > 50 else current
|
||
logger.info(f" [状态巡检] {int(time.time()-start)}s | 当前实时内容: {snippet}")
|
||
last_log = time.time()
|
||
|
||
time.sleep(5)
|
||
raise TimeoutError(f"超时: 巡检 400s 仍未发现关键字 [{expected_status}]。当前最后看到的内容: {current}")
|
||
|
||
def _click_row_action(self, machine_name, action_label):
|
||
"""通用:点击指定机器行内的操作按钮(关机/删除等)"""
|
||
logger.info(f"👉 在 {machine_name} 行内寻找并点击 [{action_label}] 按钮")
|
||
|
||
# 策略1:在包含该机器名的行内,找 aria-label 匹配的按钮
|
||
clicked = self.page.evaluate(f"""(args) => {{
|
||
const [name, action] = args;
|
||
const rows = Array.from(document.querySelectorAll('tr'));
|
||
const row = rows.find(r => r.innerText.includes(name) && r.querySelectorAll('td').length > 0);
|
||
if (!row) return 'NO_ROW';
|
||
|
||
// 尝试 aria-label 匹配
|
||
let btn = row.querySelector(`button[aria-label="${{action}}"], [aria-label="${{action}}"]`);
|
||
|
||
// 尝试按钮文本匹配
|
||
if (!btn) {{
|
||
btn = Array.from(row.querySelectorAll('button, .p-button, a, span.p-button-label'))
|
||
.find(el => el.innerText.trim() === action ||
|
||
(el.getAttribute('aria-label') || '').includes(action));
|
||
}}
|
||
|
||
if (btn) {{
|
||
btn.scrollIntoView({{block: 'center'}});
|
||
btn.click();
|
||
return 'CLICKED';
|
||
}}
|
||
return 'NO_BTN';
|
||
}}""", [machine_name, action_label])
|
||
|
||
logger.info(f" 行内按钮点击结果: {clicked}")
|
||
|
||
if clicked == 'NO_ROW':
|
||
raise Exception(f"❌ 表格中未找到包含 '{machine_name}' 的行")
|
||
if clicked == 'NO_BTN':
|
||
# 最后兜底:用 Playwright 原生按钮定位
|
||
logger.info(f" JS未命中,尝试 Playwright role 定位...")
|
||
self.page.get_by_role("button", name=action_label).first.click()
|
||
|
||
def _confirm_dialog(self):
|
||
"""确认弹窗"""
|
||
time.sleep(0.5)
|
||
try:
|
||
# PrimeVue 确认弹窗
|
||
confirm = self.page.locator('.p-confirm-dialog-accept, .p-dialog-footer button.p-button-danger, .p-dialog-footer button.p-button-primary').first
|
||
confirm.wait_for(state="visible", timeout=3000)
|
||
confirm.click()
|
||
except:
|
||
try:
|
||
self.smart_click("确定", role="button", timeout=3000)
|
||
except:
|
||
self.page.evaluate("""() => {
|
||
const btn = Array.from(document.querySelectorAll('button'))
|
||
.find(b => b.innerText.includes('确定') || b.innerText.includes('确认'));
|
||
btn?.click();
|
||
}""")
|
||
time.sleep(1)
|
||
|
||
def delete_machine(self, machine_name):
|
||
"""删除开发机"""
|
||
self.wait_for_status(machine_name, "已关机")
|
||
logger.info(f"🎯 尝试删除开发机 {machine_name}")
|
||
self._click_row_action(machine_name, "删除")
|
||
self._confirm_dialog()
|
||
time.sleep(2)
|
||
|
||
def stop_machine(self, machine_name):
|
||
"""停止/关机 开发机"""
|
||
self.wait_for_status(machine_name, "运行中")
|
||
logger.info(f"🎯 尝试下发关机指令: {machine_name}")
|
||
self._click_row_action(machine_name, "关机")
|
||
self._confirm_dialog()
|
||
time.sleep(2)
|