chore: initial baseline with P0-safety .gitignore
This commit is contained in:
@@ -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)}"}
|
||||
Reference in New Issue
Block a user