chore: initial baseline with P0-safety .gitignore
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# 联软LV7000 API集成模块
|
||||
"""
|
||||
提供联软LV7000终端安全管理系统的API客户端。
|
||||
|
||||
认证方式:三层认证(IP白名单 + 账号密码 + Token)
|
||||
- 第一层:IP白名单(在联软后台配置WhiteListServerIp)
|
||||
- 第二层:账号密码(ApiAccount + ApiPassword)
|
||||
- 第三层:一次性Token(先调getToken获取,30分钟有效)
|
||||
|
||||
核心P0接口:
|
||||
- queryDevByParams:按条件查询终端(含strusername员工账号映射)
|
||||
- getDevAllInfo:终端详细信息(硬件+软件+资产+网络)
|
||||
- getUserInfoByAccount:按账号查用户信息
|
||||
- getAllOrgInfo:全量组织架构同步
|
||||
"""
|
||||
@@ -0,0 +1,604 @@
|
||||
# 联软LV7000 API客户端
|
||||
"""
|
||||
联软LV7000终端安全管理系统 API 客户端。
|
||||
|
||||
认证流程:
|
||||
1. 第一层:IP白名单(在联软后台配置,调用时自动生效)
|
||||
2. 第二层:账号密码(ApiAccount + ApiPassword)
|
||||
3. 第三层:Token(先调getToken获取,30分钟有效,自动缓存+刷新)
|
||||
|
||||
接口调用方式:
|
||||
- GET请求:参数通过query string传递
|
||||
- POST请求:参数通过form-data传递
|
||||
- 统一携带 token + apiAccount + apiPassword + validatekey
|
||||
|
||||
使用示例:
|
||||
client = LianruanClient(base_url, api_account, api_password, validate_key)
|
||||
terminals = await client.query_dev_by_params(strusername="songxian")
|
||||
detail = await client.get_dev_all_info(strdevname="IT-SONGXIAN")
|
||||
"""
|
||||
|
||||
import time
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from app.integrations.lianruan.exceptions import (
|
||||
LianruanApiError,
|
||||
LianruanAuthError,
|
||||
LianruanConnectionError,
|
||||
)
|
||||
from app.integrations.lianruan.models import (
|
||||
TerminalBasicInfo,
|
||||
TerminalAllInfo,
|
||||
UserInfo,
|
||||
OrgDeptInfo,
|
||||
OnlineStatus,
|
||||
TerminalSoftwareInfo,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LianruanClient:
|
||||
"""联软LV7000 API客户端。
|
||||
|
||||
Attributes:
|
||||
base_url: 联软API地址,如 http://192.168.x.x:30098
|
||||
api_account: API账号
|
||||
api_password: API密码
|
||||
validate_key: 验证密钥
|
||||
_token: 缓存的Token
|
||||
_token_expire: Token过期时间戳
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
api_account: str,
|
||||
api_password: str,
|
||||
validate_key: str = "",
|
||||
timeout: float = 30.0,
|
||||
):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.api_account = api_account
|
||||
self.api_password = api_password
|
||||
self.validate_key = validate_key
|
||||
self.timeout = timeout
|
||||
|
||||
# Token缓存(30分钟有效,提前5分钟刷新)
|
||||
self._token: str = ""
|
||||
self._token_expire: float = 0.0
|
||||
|
||||
# httpx异步客户端(连接池复用)
|
||||
self._client: Optional[httpx.AsyncClient] = None
|
||||
|
||||
async def _get_client(self) -> httpx.AsyncClient:
|
||||
"""获取或创建httpx异步客户端(懒初始化+连接池复用)"""
|
||||
if self._client is None or self._client.is_closed:
|
||||
self._client = httpx.AsyncClient(
|
||||
timeout=self.timeout,
|
||||
verify=False, # 内网自签证书
|
||||
)
|
||||
return self._client
|
||||
|
||||
async def close(self) -> None:
|
||||
"""关闭httpx客户端,释放连接池"""
|
||||
if self._client and not self._client.is_closed:
|
||||
await self._client.aclose()
|
||||
|
||||
# ==========================================================================
|
||||
# Token管理
|
||||
# ==========================================================================
|
||||
|
||||
async def _ensure_token(self) -> str:
|
||||
"""确保Token有效,过期则自动刷新。
|
||||
|
||||
联软Token默认30分钟有效,提前5分钟刷新。
|
||||
|
||||
Returns:
|
||||
str: 有效的Token字符串
|
||||
"""
|
||||
now = time.time()
|
||||
# Token还有5分钟以上有效期,直接复用
|
||||
if self._token and now < self._token_expire - 300:
|
||||
return self._token
|
||||
|
||||
# 重新获取Token
|
||||
logger.info("联软Token过期或为空,正在刷新...")
|
||||
try:
|
||||
client = await self._get_client()
|
||||
url = f"{self.base_url}/token"
|
||||
params = {
|
||||
"act": "getToken",
|
||||
"apiAccount": self.api_account,
|
||||
"apiPassword": self.api_password,
|
||||
}
|
||||
if self.validate_key:
|
||||
params["validatekey"] = self.validate_key
|
||||
|
||||
resp = await client.get(url, params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
if data.get("status") != "SUCCESS":
|
||||
raise LianruanAuthError(
|
||||
f"获取Token失败: {data.get('msg', '未知错误')}",
|
||||
detail=str(data),
|
||||
)
|
||||
|
||||
self._token = data.get("data", data.get("rows", ""))
|
||||
if not self._token:
|
||||
# 有些版本返回格式不同
|
||||
self._token = str(data.get("token", ""))
|
||||
|
||||
# 30分钟有效期
|
||||
self._token_expire = now + 1800
|
||||
logger.info("联软Token刷新成功,有效期至 %s",
|
||||
time.strftime("%H:%M:%S", time.localtime(self._token_expire)))
|
||||
return self._token
|
||||
|
||||
except httpx.ConnectError as e:
|
||||
raise LianruanConnectionError(
|
||||
f"无法连接联软服务器 {self.base_url}: {e}",
|
||||
detail=str(e),
|
||||
)
|
||||
except httpx.TimeoutException as e:
|
||||
raise LianruanConnectionError(
|
||||
f"连接联软服务器超时: {e}",
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# 通用请求方法
|
||||
# ==========================================================================
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
path: str,
|
||||
act: str,
|
||||
params: Optional[dict] = None,
|
||||
method: str = "GET",
|
||||
) -> dict:
|
||||
"""发送请求到联软API。
|
||||
|
||||
自动携带认证参数(token + apiAccount + apiPassword)。
|
||||
|
||||
Args:
|
||||
path: API路径,如 /terminal 或 /querydeptuser
|
||||
act: 操作类型,如 queryDevByParams
|
||||
params: 额外业务参数
|
||||
method: 请求方法(GET/POST)
|
||||
|
||||
Returns:
|
||||
dict: 联软API返回的JSON数据
|
||||
|
||||
Raises:
|
||||
LianruanAuthError: 认证失败
|
||||
LianruanApiError: 业务错误
|
||||
LianruanConnectionError: 网络错误
|
||||
"""
|
||||
token = await self._ensure_token()
|
||||
client = await self._get_client()
|
||||
|
||||
# 构建完整参数:认证参数 + 业务参数
|
||||
full_params = {
|
||||
"act": act,
|
||||
"apiAccount": self.api_account,
|
||||
"apiPassword": self.api_password,
|
||||
"token": token,
|
||||
}
|
||||
if self.validate_key:
|
||||
full_params["validatekey"] = self.validate_key
|
||||
if params:
|
||||
full_params.update(params)
|
||||
|
||||
url = f"{self.base_url}{path}"
|
||||
|
||||
try:
|
||||
if method.upper() == "POST":
|
||||
resp = await client.post(url, data=full_params)
|
||||
else:
|
||||
resp = await client.get(url, params=full_params)
|
||||
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
except httpx.ConnectError as e:
|
||||
raise LianruanConnectionError(
|
||||
f"无法连接联软服务器: {e}",
|
||||
detail=str(e),
|
||||
)
|
||||
except httpx.TimeoutException as e:
|
||||
raise LianruanConnectionError(
|
||||
f"请求联软超时: {e}",
|
||||
detail=str(e),
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise LianruanApiError(
|
||||
f"联软HTTP错误 {e.response.status_code}",
|
||||
status=str(e.response.status_code),
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
# 检查联软业务状态码
|
||||
status = data.get("status", "")
|
||||
if status == "INVALID":
|
||||
# Token可能过期,清除缓存重试一次
|
||||
self._token = ""
|
||||
self._token_expire = 0
|
||||
raise LianruanAuthError(
|
||||
f"联软认证失败(IP不在白名单或Token无效): {data.get('msg', '')}",
|
||||
detail=str(data),
|
||||
)
|
||||
elif status == "ERROR":
|
||||
raise LianruanApiError(
|
||||
f"联软API错误: {data.get('msg', '')}",
|
||||
status=status,
|
||||
detail=str(data),
|
||||
)
|
||||
elif status == "Exceed":
|
||||
raise LianruanApiError(
|
||||
f"联软数据量超限: {data.get('msg', '')}",
|
||||
status=status,
|
||||
detail=str(data),
|
||||
)
|
||||
elif status != "SUCCESS":
|
||||
raise LianruanApiError(
|
||||
f"联软未知状态: {status} - {data.get('msg', '')}",
|
||||
status=status,
|
||||
detail=str(data),
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
# ==========================================================================
|
||||
# P0接口 — 终端设备查询
|
||||
# ==========================================================================
|
||||
|
||||
async def query_dev_by_params(
|
||||
self,
|
||||
strusername: str = "",
|
||||
strdevname: str = "",
|
||||
strdevip: str = "",
|
||||
strmac: str = "",
|
||||
page: int = 1,
|
||||
per_page: int = 20,
|
||||
) -> dict:
|
||||
"""查询终端设备(核心映射接口)。
|
||||
|
||||
⭐ strusername 参数可直接按员工账号查终端,这是联软最大的优势!
|
||||
|
||||
Args:
|
||||
strusername: 员工账号(映射金钥匙)
|
||||
strdevname: 计算机名
|
||||
strdevip: IP地址
|
||||
strmac: MAC地址
|
||||
page: 页码(从1开始)
|
||||
per_page: 每页条数
|
||||
|
||||
Returns:
|
||||
dict: {"items": [TerminalBasicInfo], "total": int}
|
||||
"""
|
||||
params: dict = {}
|
||||
if strusername:
|
||||
params["strusername"] = strusername
|
||||
if strdevname:
|
||||
params["strdevname"] = strdevname
|
||||
if strdevip:
|
||||
params["strdevip"] = strdevip
|
||||
if strmac:
|
||||
params["strmac"] = strmac
|
||||
|
||||
# 联软分页参数
|
||||
params["page"] = str(page)
|
||||
params["rows"] = str(per_page)
|
||||
|
||||
data = await self._request("/terminal", "queryDevByParams", params)
|
||||
|
||||
rows = data.get("rows", [])
|
||||
total = data.get("total", len(rows))
|
||||
items = [TerminalBasicInfo(**row) for row in rows]
|
||||
|
||||
return {"items": items, "total": total}
|
||||
|
||||
async def get_dev_all_info(
|
||||
self,
|
||||
strdevname: str = "",
|
||||
strdevip: str = "",
|
||||
) -> TerminalAllInfo:
|
||||
"""查询终端详细信息(极详细硬件+软件+资产+网络)。
|
||||
|
||||
比火绒_info2更丰富,包含逻辑磁盘使用率、显示器信息、内存条详情。
|
||||
|
||||
Args:
|
||||
strdevname: 计算机名(二选一)
|
||||
strdevip: IP地址(二选一)
|
||||
|
||||
Returns:
|
||||
TerminalAllInfo: 终端详细信息
|
||||
"""
|
||||
params: dict = {}
|
||||
if strdevname:
|
||||
params["strdevname"] = strdevname
|
||||
if strdevip:
|
||||
params["strdevip"] = strdevip
|
||||
|
||||
data = await self._request(
|
||||
"/devallinfoshowwithpaging", "getDevAllInfo", params
|
||||
)
|
||||
|
||||
# 返回格式:data.equipment + data.equipmentdetail
|
||||
equipment = data.get("equipment", data.get("rows", [{}]))
|
||||
if isinstance(equipment, list) and equipment:
|
||||
equipment = equipment[0]
|
||||
|
||||
equipment_detail = data.get("equipmentdetail", {})
|
||||
if isinstance(equipment_detail, list) and equipment_detail:
|
||||
equipment_detail = equipment_detail[0]
|
||||
dev_detail = equipment_detail.get("devdetail", equipment_detail)
|
||||
|
||||
# 解析硬件详情
|
||||
result = TerminalAllInfo(
|
||||
strdevname=equipment.get("strdevname", ""),
|
||||
strip1=equipment.get("strip1", ""),
|
||||
strmac=equipment.get("strmac", ""),
|
||||
strdeptname=equipment.get("strdeptname", ""),
|
||||
strusername=equipment.get("strusername", ""),
|
||||
struserdes=equipment.get("struserdes", ""),
|
||||
stros=equipment.get("stros", ""),
|
||||
strdomain=equipment.get("strdomain", ""),
|
||||
istatus=dev_detail.get("istatus", equipment.get("istatus", "")),
|
||||
strverofuaagent=dev_detail.get("strverofuaagent", ""),
|
||||
devassetno=dev_detail.get("devassetno", ""),
|
||||
devgroup=dev_detail.get("devgroup", ""),
|
||||
)
|
||||
|
||||
# 解析硬件列表
|
||||
self._parse_hardware_list(dev_detail, "CPUInformation", result.cpu)
|
||||
self._parse_hardware_list(dev_detail, "MemoryInformation", result.memory)
|
||||
self._parse_hardware_list(dev_detail, "HardDiskInformation", result.hard_disk)
|
||||
self._parse_hardware_list(dev_detail, "GraphicsCardInformation", result.graphics_card)
|
||||
self._parse_hardware_list(dev_detail, "MainboardInformation", result.mainboard)
|
||||
|
||||
# 解析逻辑磁盘(含使用率)
|
||||
for ld in dev_detail.get("LogicalDiskInformation", []):
|
||||
result.logical_disk.append(LogicalDiskInfo(
|
||||
name=ld.get("strlogicaldiskname", ""),
|
||||
file_system=ld.get("strfilesystem", ""),
|
||||
total_size=ld.get("strtotalsize", ""),
|
||||
free_space=ld.get("strfreespace", ""),
|
||||
usage_percent=ld.get("strusagepercent", ""),
|
||||
))
|
||||
|
||||
# 解析网卡
|
||||
for nc in dev_detail.get("NetworkCardInformation", []):
|
||||
result.network_card.append(NetworkCardInfo(
|
||||
name=nc.get("strnetcardname", ""),
|
||||
is_wireless=nc.get("iswireless", ""),
|
||||
vendor=nc.get("strnetcardvendor", ""),
|
||||
mac=nc.get("strnetcardmac", ""),
|
||||
))
|
||||
|
||||
# 解析显示器
|
||||
for d in dev_detail.get("DisplayInformation", []):
|
||||
result.display.append(DisplayInfo(
|
||||
vendor=d.get("strdisplayvendor", ""),
|
||||
model=d.get("strdisplaymodel", ""),
|
||||
serial=d.get("strdisplayserial", ""),
|
||||
size=d.get("strdisplaysize", ""),
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
def _parse_hardware_list(
|
||||
self, dev_detail: dict, key: str, target_list: list
|
||||
) -> None:
|
||||
"""解析硬件信息列表(CPU/内存/硬盘等)"""
|
||||
from app.integrations.lianruan.models import HardwareInfo
|
||||
|
||||
for item in dev_detail.get(key, []):
|
||||
target_list.append(HardwareInfo(
|
||||
name=item.get("strcpuname", item.get("strmemname", item.get("strdiskname", ""))),
|
||||
model=item.get("strcpumodel", item.get("strmemmodel", item.get("strdiskmodel", ""))),
|
||||
vendor=item.get("strcpuvendor", item.get("strmemvendor", item.get("strdiskvendor", ""))),
|
||||
capacity=item.get("strcpufrequency", item.get("strmemcapacity", item.get("strdiskcapacity", ""))),
|
||||
serial=item.get("strcpuserial", item.get("strmemserial", item.get("strdiskserial", ""))),
|
||||
))
|
||||
|
||||
# ==========================================================================
|
||||
# P0接口 — 组织架构/用户
|
||||
# ==========================================================================
|
||||
|
||||
async def get_user_info_by_account(self, useraccount: str) -> Optional[UserInfo]:
|
||||
"""按账号查询用户信息。
|
||||
|
||||
Args:
|
||||
useraccount: 用户账号
|
||||
|
||||
Returns:
|
||||
UserInfo或None
|
||||
"""
|
||||
data = await self._request(
|
||||
"/querydeptuser",
|
||||
"getUserInfoByAccount",
|
||||
{"useraccount": useraccount},
|
||||
)
|
||||
rows = data.get("rows", data.get("row", []))
|
||||
if rows:
|
||||
row = rows[0] if isinstance(rows, list) else rows
|
||||
return UserInfo(**row)
|
||||
return None
|
||||
|
||||
async def get_all_org_info(self) -> list[OrgDeptInfo]:
|
||||
"""获取全量组织架构(部门+用户)。
|
||||
|
||||
用于定时同步,构建组织架构映射。
|
||||
|
||||
Returns:
|
||||
list[OrgDeptInfo]: 部门列表,每个部门含用户列表
|
||||
"""
|
||||
data = await self._request("/querydeptuser", "getAllOrgInfo")
|
||||
rows = data.get("rows", [])
|
||||
result = []
|
||||
for dept_data in rows:
|
||||
users = []
|
||||
for u in dept_data.get("users", []):
|
||||
users.append(UserInfo(**u))
|
||||
result.append(OrgDeptInfo(
|
||||
deptid=dept_data.get("deptid", ""),
|
||||
deptname=dept_data.get("deptname", ""),
|
||||
parentid=dept_data.get("parentid", ""),
|
||||
users=users,
|
||||
))
|
||||
return result
|
||||
|
||||
# ==========================================================================
|
||||
# P1接口 — 准入控制
|
||||
# ==========================================================================
|
||||
|
||||
async def exist_online_user(
|
||||
self, username: str, strdevip: str = ""
|
||||
) -> OnlineStatus:
|
||||
"""查询终端用户是否在线。
|
||||
|
||||
可精确判断某员工在某IP是否当前在线。
|
||||
|
||||
Args:
|
||||
username: 用户名
|
||||
strdevip: IP地址(可选)
|
||||
|
||||
Returns:
|
||||
OnlineStatus: 在线状态
|
||||
"""
|
||||
params = {"username": username}
|
||||
if strdevip:
|
||||
params["strdevip"] = strdevip
|
||||
|
||||
data = await self._request(
|
||||
"/access/onlineUser", "existOnlineUser", params
|
||||
)
|
||||
is_online = data.get("data", "0") == "1"
|
||||
return OnlineStatus(
|
||||
username=username,
|
||||
ip=strdevip,
|
||||
is_online=is_online,
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# P1接口 — 终端操作
|
||||
# ==========================================================================
|
||||
|
||||
async def notice_agent_msg(
|
||||
self, strdevip: str, message: str
|
||||
) -> bool:
|
||||
"""向终端推送弹窗消息。
|
||||
|
||||
Args:
|
||||
strdevip: 终端IP
|
||||
message: 消息内容
|
||||
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
"""
|
||||
data = await self._request(
|
||||
"/terminal",
|
||||
"noticeAgentMsg",
|
||||
{"strdevip": strdevip, "msg": message},
|
||||
)
|
||||
return data.get("status") == "SUCCESS"
|
||||
|
||||
async def remote_wake_up(
|
||||
self, strdevip: str, strmac: str
|
||||
) -> bool:
|
||||
"""远程唤醒终端。
|
||||
|
||||
Args:
|
||||
strdevip: 终端IP
|
||||
strmac: 终端MAC地址
|
||||
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
"""
|
||||
data = await self._request(
|
||||
"/terminal",
|
||||
"remoteWakeUp",
|
||||
{"strdevip": strdevip, "strmac": strmac},
|
||||
)
|
||||
return data.get("status") == "SUCCESS"
|
||||
|
||||
async def query_software_by_dev(
|
||||
self, strdevname: str = "", strdevip: str = ""
|
||||
) -> Optional[TerminalSoftwareInfo]:
|
||||
"""查询终端安装软件。
|
||||
|
||||
Args:
|
||||
strdevname: 计算机名
|
||||
strdevip: IP地址
|
||||
|
||||
Returns:
|
||||
TerminalSoftwareInfo或None
|
||||
"""
|
||||
params: dict = {}
|
||||
if strdevname:
|
||||
params["strdevname"] = strdevname
|
||||
if strdevip:
|
||||
params["strdevip"] = strdevip
|
||||
|
||||
data = await self._request("/software", "querysoftwarebydev", params)
|
||||
rows = data.get("rows", [])
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
row = rows[0] if isinstance(rows, list) else rows
|
||||
softwares = []
|
||||
for s in row.get("softwares", []):
|
||||
softwares.append(SoftwareInfo(
|
||||
name=s.get("strsoftware", ""),
|
||||
version=s.get("strversion", ""),
|
||||
vendor=s.get("strvendor", ""),
|
||||
install_date=s.get("installdate", ""),
|
||||
))
|
||||
return TerminalSoftwareInfo(
|
||||
strdevname=row.get("strdevname", ""),
|
||||
strdevip=row.get("strdevip", ""),
|
||||
strmac=row.get("strmac", ""),
|
||||
strusername=row.get("strusername", ""),
|
||||
softwares=softwares,
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# 测试连接
|
||||
# ==========================================================================
|
||||
|
||||
async def test_connection(self) -> dict:
|
||||
"""测试联软API连接。
|
||||
|
||||
使用getToken接口验证:
|
||||
1. 网络连通性
|
||||
2. IP白名单
|
||||
3. 账号密码正确性
|
||||
4. Token获取成功
|
||||
|
||||
Returns:
|
||||
dict: {"success": bool, "message": str}
|
||||
"""
|
||||
try:
|
||||
token = await self._ensure_token()
|
||||
if token:
|
||||
return {
|
||||
"success": True,
|
||||
"message": "联软API连接成功,Token获取正常",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Token获取失败,返回为空",
|
||||
}
|
||||
except LianruanAuthError as e:
|
||||
return {"success": False, "message": e.message}
|
||||
except LianruanConnectionError as e:
|
||||
return {"success": False, "message": e.message}
|
||||
except Exception as e:
|
||||
return {"success": False, "message": f"未知错误: {str(e)}"}
|
||||
@@ -0,0 +1,98 @@
|
||||
# 联软LV7000配置管理
|
||||
"""
|
||||
从system_configs表读取联软API配置,构建LianruanClient实例。
|
||||
|
||||
联软配置键(前缀 integration_lianruan_):
|
||||
- integration_lianruan_base_url: 联软API地址(如 http://192.168.x.x:30098)
|
||||
- integration_lianruan_api_account: API账号
|
||||
- integration_lianruan_api_password: API密码
|
||||
- integration_lianruan_validate_key: 验证密钥(可选)
|
||||
|
||||
配置方式:管理后台 → 系统集成 → 联软LV7000 → 填入账号密码
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.integrations.lianruan.client import LianruanClient
|
||||
from app.integrations.lianruan.exceptions import LianruanConfigError
|
||||
from app.models.system_config import SystemConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 联软配置键前缀(与 admin_service INTEGRATION_DEFINITIONS 中的 key_prefix 一致)
|
||||
_PREFIX = "integration_lianruan_"
|
||||
|
||||
|
||||
async def _get_lianruan_config_value(db: AsyncSession, key_suffix: str) -> str:
|
||||
"""读取单个联软配置值。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
key_suffix: 配置键后缀(如 base_url / api_account)
|
||||
|
||||
Returns:
|
||||
str: 配置值,不存在返回空字符串
|
||||
"""
|
||||
full_key = f"{_PREFIX}{key_suffix}"
|
||||
from sqlalchemy import select
|
||||
result = await db.execute(select(SystemConfig).where(SystemConfig.key == full_key))
|
||||
config_row = result.scalar_one_or_none()
|
||||
return config_row.value if config_row else ""
|
||||
|
||||
|
||||
async def get_lianruan_config(db: AsyncSession) -> dict:
|
||||
"""从system_configs表读取联软配置。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
dict: 包含 base_url / api_account / api_password / validate_key
|
||||
|
||||
Raises:
|
||||
LianruanConfigError: 配置缺失
|
||||
"""
|
||||
base_url = await _get_lianruan_config_value(db, "base_url")
|
||||
api_account = await _get_lianruan_config_value(db, "api_account")
|
||||
api_password = await _get_lianruan_config_value(db, "api_password")
|
||||
validate_key = await _get_lianruan_config_value(db, "validate_key")
|
||||
|
||||
if not base_url:
|
||||
raise LianruanConfigError("联软API未配置:缺少Base URL")
|
||||
if not api_account:
|
||||
raise LianruanConfigError("联软API未配置:缺少API账号")
|
||||
if not api_password:
|
||||
raise LianruanConfigError("联软API未配置:缺少API密码")
|
||||
|
||||
return {
|
||||
"base_url": base_url,
|
||||
"api_account": api_account,
|
||||
"api_password": api_password,
|
||||
"validate_key": validate_key,
|
||||
}
|
||||
|
||||
|
||||
async def get_lianruan_client(db: AsyncSession) -> LianruanClient:
|
||||
"""构建联软API客户端实例。
|
||||
|
||||
从system_configs表读取配置,创建LianruanClient。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
LianruanClient: 已配置的联软客户端
|
||||
|
||||
Raises:
|
||||
LianruanConfigError: 配置缺失
|
||||
"""
|
||||
cfg = await get_lianruan_config(db)
|
||||
|
||||
return LianruanClient(
|
||||
base_url=cfg["base_url"],
|
||||
api_account=cfg["api_account"],
|
||||
api_password=cfg["api_password"],
|
||||
validate_key=cfg.get("validate_key", ""),
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
# 联软LV7000异常体系
|
||||
"""
|
||||
定义联软API集成的异常类层级。
|
||||
|
||||
层级:
|
||||
LianruanError — 基类(所有联软异常)
|
||||
├── LianruanConfigError — 配置缺失(未填写账号/密码/BaseURL)
|
||||
├── LianruanAuthError — 认证失败(IP不在白名单/账号密码错误/Token过期)
|
||||
├── LianruanConnectionError — 网络连接失败(超时/拒绝连接)
|
||||
└── LianruanApiError — API业务错误(参数错误/数据超限/其他)
|
||||
"""
|
||||
|
||||
|
||||
class LianruanError(Exception):
|
||||
"""联软异常基类"""
|
||||
|
||||
def __init__(self, message: str, detail: str = ""):
|
||||
self.message = message
|
||||
self.detail = detail
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class LianruanConfigError(LianruanError):
|
||||
"""配置缺失异常。
|
||||
|
||||
场景:未配置联软 BaseURL / ApiAccount / ApiPassword
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class LianruanAuthError(LianruanError):
|
||||
"""认证失败异常。
|
||||
|
||||
场景:
|
||||
- IP不在白名单(status=INVALID)
|
||||
- 账号密码错误
|
||||
- Token过期(需重新获取)
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class LianruanConnectionError(LianruanError):
|
||||
"""网络连接失败异常。
|
||||
|
||||
场景:超时/拒绝连接/DNS解析失败
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class LianruanApiError(LianruanError):
|
||||
"""API业务错误异常。
|
||||
|
||||
场景:
|
||||
- 参数错误(status=ERROR)
|
||||
- 数据量超限(status=Exceed)
|
||||
- 其他业务异常
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, status: str = "", detail: str = ""):
|
||||
self.status = status # 联软返回的status字段(ERROR/Exceed等)
|
||||
super().__init__(message, detail)
|
||||
@@ -0,0 +1,193 @@
|
||||
# 联软LV7000数据模型
|
||||
"""
|
||||
定义联软API返回数据的Pydantic模型。
|
||||
|
||||
核心模型:
|
||||
- TerminalBasicInfo:终端基本信息(queryDevByParams返回)
|
||||
- TerminalAllInfo:终端详细信息(getDevAllInfo返回,极详细)
|
||||
- UserInfo:用户信息(getUserInfoByAccount返回)
|
||||
- OrgInfo:组织架构信息(getAllOrgInfo返回)
|
||||
- OnlineStatus:终端在线状态(existOnlineUser返回)
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 终端基本信息(queryDevByParams返回)
|
||||
# ==========================================================================
|
||||
|
||||
class TerminalBasicInfo(BaseModel):
|
||||
"""终端基本信息 — 最核心的映射数据源。
|
||||
|
||||
⭐ strusername + struserdes 字段直接提供员工账号→终端映射!
|
||||
这是联软相比火绒最大的优势。
|
||||
"""
|
||||
# 终端标识
|
||||
strdevname: str = Field(default="", description="计算机名")
|
||||
strdevip: str = Field(default="", description="IP地址")
|
||||
strmac: str = Field(default="", description="MAC地址")
|
||||
|
||||
# ⭐ 员工映射字段(核心价值)
|
||||
strusername: str = Field(default="", description="使用该终端的用户账号(映射金钥匙)")
|
||||
struserdes: str = Field(default="", description="用户姓名/描述")
|
||||
|
||||
# 组织信息
|
||||
strdeptname: str = Field(default="", description="所属部门名")
|
||||
|
||||
# 状态
|
||||
istatus: str = Field(default="", description="终端状态(1=在线/0=离线)")
|
||||
|
||||
# 网络
|
||||
strswitchname: str = Field(default="", description="接入交换机名")
|
||||
strifname: str = Field(default="", description="交换机接口名")
|
||||
|
||||
# 联系方式
|
||||
strmail: str = Field(default="", description="用户邮箱")
|
||||
strphone: str = Field(default="", description="用户电话")
|
||||
|
||||
# 其他
|
||||
strdomain: str = Field(default="", description="Windows域")
|
||||
strdevtype: str = Field(default="", description="设备类型")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 终端详细信息(getDevAllInfo返回)
|
||||
# ==========================================================================
|
||||
|
||||
class HardwareInfo(BaseModel):
|
||||
"""硬件组件信息"""
|
||||
name: str = Field(default="", description="名称")
|
||||
model: str = Field(default="", description="型号")
|
||||
vendor: str = Field(default="", description="厂商")
|
||||
capacity: str = Field(default="", description="容量")
|
||||
serial: str = Field(default="", description="序列号")
|
||||
|
||||
|
||||
class LogicalDiskInfo(BaseModel):
|
||||
"""逻辑磁盘信息(含使用率,判断磁盘满)"""
|
||||
name: str = Field(default="", description="卷标")
|
||||
file_system: str = Field(default="", description="文件系统")
|
||||
total_size: str = Field(default="", description="总量")
|
||||
free_space: str = Field(default="", description="可用空间")
|
||||
usage_percent: str = Field(default="", description="使用率")
|
||||
|
||||
|
||||
class NetworkCardInfo(BaseModel):
|
||||
"""网卡信息"""
|
||||
name: str = Field(default="", description="名称")
|
||||
is_wireless: str = Field(default="", description="是否无线")
|
||||
vendor: str = Field(default="", description="厂商")
|
||||
mac: str = Field(default="", description="MAC地址")
|
||||
|
||||
|
||||
class DisplayInfo(BaseModel):
|
||||
"""显示器信息(多屏配置排查)"""
|
||||
vendor: str = Field(default="", description="厂商")
|
||||
model: str = Field(default="", description="型号")
|
||||
serial: str = Field(default="", description="序列号")
|
||||
size: str = Field(default="", description="尺寸")
|
||||
|
||||
|
||||
class TerminalAllInfo(BaseModel):
|
||||
"""终端详细信息 — 极其详细,比火绒_info2更丰富。
|
||||
|
||||
包含:设备基础+硬件+软件+资产+网络配置。
|
||||
特别是逻辑磁盘使用率和显示器信息,是火绒没有的。
|
||||
"""
|
||||
# 设备基础
|
||||
strdevname: str = Field(default="", description="计算机名")
|
||||
strip1: str = Field(default="", description="IP地址")
|
||||
strmac: str = Field(default="", description="MAC地址")
|
||||
strnatip: str = Field(default="", description="NAT IP")
|
||||
macverdor: str = Field(default="", description="MAC厂商")
|
||||
strdevtype: str = Field(default="", description="设备类型")
|
||||
|
||||
# 组织+用户
|
||||
strdeptname: str = Field(default="", description="所属部门")
|
||||
strusername: str = Field(default="", description="用户账号⭐")
|
||||
struserdes: str = Field(default="", description="用户姓名⭐")
|
||||
|
||||
# 时间
|
||||
dtdevuptime: str = Field(default="", description="最近上线时间")
|
||||
dtdevdowntime: str = Field(default="", description="最近下线时间")
|
||||
dtdevfirstfoundtime: str = Field(default="", description="首次发现时间")
|
||||
|
||||
# 系统
|
||||
stros: str = Field(default="", description="操作系统")
|
||||
strdomain: str = Field(default="", description="Windows域")
|
||||
strserialnumber: str = Field(default="", description="序列号")
|
||||
strmainboardtype: str = Field(default="", description="主板型号")
|
||||
|
||||
# 客户端详情
|
||||
strverofuaagent: str = Field(default="", description="安全助手版本")
|
||||
istatus: str = Field(default="", description="在线状态")
|
||||
devassetno: str = Field(default="", description="设备资产号")
|
||||
devgroup: str = Field(default="", description="设备所属设备组")
|
||||
|
||||
# 硬件详情(列表)
|
||||
mainboard: list[HardwareInfo] = Field(default_factory=list, description="主板信息")
|
||||
cpu: list[HardwareInfo] = Field(default_factory=list, description="CPU信息")
|
||||
memory: list[HardwareInfo] = Field(default_factory=list, description="内存信息")
|
||||
hard_disk: list[HardwareInfo] = Field(default_factory=list, description="硬盘信息")
|
||||
logical_disk: list[LogicalDiskInfo] = Field(default_factory=list, description="逻辑磁盘")
|
||||
graphics_card: list[HardwareInfo] = Field(default_factory=list, description="显卡信息")
|
||||
network_card: list[NetworkCardInfo] = Field(default_factory=list, description="网卡信息")
|
||||
display: list[DisplayInfo] = Field(default_factory=list, description="显示器信息")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 用户信息(getUserInfoByAccount返回)
|
||||
# ==========================================================================
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
"""用户信息"""
|
||||
deptid: str = Field(default="", description="部门ID")
|
||||
userid: str = Field(default="", description="用户ID")
|
||||
useraccount: str = Field(default="", description="用户账号")
|
||||
username: str = Field(default="", description="用户姓名")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 组织架构信息(getAllOrgInfo返回)
|
||||
# ==========================================================================
|
||||
|
||||
class OrgDeptInfo(BaseModel):
|
||||
"""部门信息"""
|
||||
deptid: str = Field(default="", description="部门ID")
|
||||
deptname: str = Field(default="", description="部门名称")
|
||||
parentid: str = Field(default="", description="父部门ID")
|
||||
users: list[UserInfo] = Field(default_factory=list, description="部门下用户列表")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 终端在线状态(existOnlineUser返回)
|
||||
# ==========================================================================
|
||||
|
||||
class OnlineStatus(BaseModel):
|
||||
"""终端在线状态"""
|
||||
username: str = Field(default="", description="用户名")
|
||||
ip: str = Field(default="", description="IP地址")
|
||||
is_online: bool = Field(default=False, description="是否在线")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 软件信息(querysoftwarebydev返回)
|
||||
# ==========================================================================
|
||||
|
||||
class SoftwareInfo(BaseModel):
|
||||
"""软件安装信息"""
|
||||
name: str = Field(default="", description="软件名称")
|
||||
version: str = Field(default="", description="版本")
|
||||
vendor: str = Field(default="", description="厂商")
|
||||
install_date: str = Field(default="", description="安装日期")
|
||||
|
||||
|
||||
class TerminalSoftwareInfo(BaseModel):
|
||||
"""终端安装软件信息"""
|
||||
strdevname: str = Field(default="", description="计算机名")
|
||||
strdevip: str = Field(default="", description="IP地址")
|
||||
strmac: str = Field(default="", description="MAC地址")
|
||||
strusername: str = Field(default="", description="用户账号")
|
||||
softwares: list[SoftwareInfo] = Field(default_factory=list, description="软件列表")
|
||||
Reference in New Issue
Block a user