1. 已基本完成功能,实现web server显示使用情况,实现排队的显示。实现lic的派发功能
2. 遗留有lic client的处理存在无法中断的问题。
This commit is contained in:
parent
552828ba21
commit
89e6c9439c
135
lic_info.json
Executable file
135
lic_info.json
Executable file
|
@ -0,0 +1,135 @@
|
|||
{
|
||||
"27003@szmaslic03": {
|
||||
"name": "27003@szmaslic03",
|
||||
"total": 2,
|
||||
"used": 2,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "haoxiang.ran",
|
||||
"host": "szl3bc12808",
|
||||
"lic": "szmaslic03",
|
||||
"login": "0.41h"
|
||||
},
|
||||
{
|
||||
"user": "ekko.bao",
|
||||
"host": "qwerqwerq",
|
||||
"lic": "27003@szmaslic03",
|
||||
"login": "8.00h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic04": {
|
||||
"name": "27003@szmaslic04",
|
||||
"total": 2,
|
||||
"used": 2,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "macro.yang",
|
||||
"host": "szl3bc12804",
|
||||
"lic": "szmaslic04",
|
||||
"login": "52.28h"
|
||||
},
|
||||
{
|
||||
"user": "asdfsadf",
|
||||
"host": "fdgh",
|
||||
"lic": "27003@szmaslic04",
|
||||
"login": "1.77h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic06": {
|
||||
"name": "27003@szmaslic06",
|
||||
"total": 2,
|
||||
"used": 2,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "zhenhao.he",
|
||||
"host": "szl3bc12804",
|
||||
"lic": "szmaslic06",
|
||||
"login": "0.20h"
|
||||
},
|
||||
{
|
||||
"user": "phillip.chan",
|
||||
"host": "szl3bc12810",
|
||||
"lic": "szmaslic06",
|
||||
"login": "0.16h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic07": {
|
||||
"name": "27003@szmaslic07",
|
||||
"total": 2,
|
||||
"used": 2,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "zhiyi.li",
|
||||
"host": "szl3bc12806",
|
||||
"lic": "szmaslic07",
|
||||
"login": "2.96h"
|
||||
},
|
||||
{
|
||||
"user": "kkkkkkkkkk",
|
||||
"host": "dddddd",
|
||||
"lic": "27003@szmaslic07",
|
||||
"login": "3.04h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic08": {
|
||||
"name": "27003@szmaslic08",
|
||||
"total": 2,
|
||||
"used": 2,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "soo.liu",
|
||||
"host": "szl3bc12809",
|
||||
"lic": "szmaslic08",
|
||||
"login": "6.55h"
|
||||
},
|
||||
{
|
||||
"user": "haichao.ou",
|
||||
"host": "szl3bc12806",
|
||||
"lic": "szmaslic08",
|
||||
"login": "5.93h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic09": {
|
||||
"name": "27003@szmaslic09",
|
||||
"total": 2,
|
||||
"used": 2,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "louie.liang",
|
||||
"host": "szl3bc12810",
|
||||
"lic": "szmaslic09",
|
||||
"login": "8.31h"
|
||||
},
|
||||
{
|
||||
"user": "zabbix",
|
||||
"host": "szl3bc06409",
|
||||
"lic": "szmaslic09",
|
||||
"login": "0.90h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic10": {
|
||||
"name": "27003@szmaslic10",
|
||||
"total": 2,
|
||||
"used": 2,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "harvey.li",
|
||||
"host": "szl3bc12810",
|
||||
"lic": "szmaslic10",
|
||||
"login": "57.38h"
|
||||
},
|
||||
{
|
||||
"user": "kw.hu",
|
||||
"host": "szl3bc12809",
|
||||
"lic": "szmaslic10",
|
||||
"login": "1.41h"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
475
license.py
Normal file → Executable file
475
license.py
Normal file → Executable file
|
@ -1,25 +1,256 @@
|
|||
#!/usr/bin/python3
|
||||
import json
|
||||
import queue
|
||||
import re
|
||||
import sys
|
||||
import socket
|
||||
import asyncio
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Optional, Union
|
||||
from datetime import datetime
|
||||
from utils import *
|
||||
import logger
|
||||
log = logger.get_logger()
|
||||
from my_log import get_logger
|
||||
import socket
|
||||
import time
|
||||
import threading
|
||||
import select
|
||||
log = get_logger()
|
||||
|
||||
|
||||
MOCK_LMUTIL = True
|
||||
if MOCK_LMUTIL:
|
||||
from mock_lmutil import *
|
||||
|
||||
DISPATCHER_SERVER_PORT = 8809
|
||||
|
||||
class LicenseDispatcher:
|
||||
def __init__(self, host='0.0.0.0', port=DISPATCHER_SERVER_PORT):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.server_socket.bind((self.host, self.port))
|
||||
self.server_socket.listen(5)
|
||||
self.inputs = [self.server_socket] # all socket to listen
|
||||
self.outputs = [] # all socket to write
|
||||
self.message_queues = {} # store message to send
|
||||
self.lock = threading.Lock()
|
||||
self.wait_queue = Queue()
|
||||
self.exit_event = threading.Event()
|
||||
self.loop_thread = threading.Thread(target=self.loop)
|
||||
self.loop_thread.start()
|
||||
|
||||
def exit(self):
|
||||
self.exit_event.set()
|
||||
self.loop_thread.join()
|
||||
|
||||
def loop(self):
|
||||
log.info(f"dispatcher server start, listen {self.host}:{self.port}")
|
||||
while not self.exit_event.is_set():
|
||||
# select listen all socket read/write event
|
||||
readable, writable, exceptional = select.select(self.inputs, self.outputs, self.inputs, 1)
|
||||
# handle readable socket
|
||||
for sock in readable:
|
||||
if sock is self.server_socket:
|
||||
# new client connect
|
||||
self._handle_new_connection()
|
||||
else:
|
||||
# handle client data
|
||||
self._handle_client_data(sock)
|
||||
# handle writable socket
|
||||
for sock in writable:
|
||||
self._handle_client_response(sock)
|
||||
# handle exception socket
|
||||
for sock in exceptional:
|
||||
self._handle_socket_exception(sock)
|
||||
# clean inactive client
|
||||
self._cleanup_inactive_clients()
|
||||
log.debug("LicenseDispatcher exit")
|
||||
|
||||
def _handle_new_connection(self):
|
||||
with self.lock:
|
||||
client_socket, address = self.server_socket.accept()
|
||||
log.info(f"new client connect: {address}")
|
||||
client_socket.setblocking(0)
|
||||
self.inputs.append(client_socket)
|
||||
self.message_queues[client_socket] = []
|
||||
|
||||
def _handle_client_data(self, sock):
|
||||
try:
|
||||
data = sock.recv(1024)
|
||||
if not data:
|
||||
return
|
||||
log.trace(f"recv data: {data}")
|
||||
try:
|
||||
req = json.loads(data.decode('utf-8'))
|
||||
except json.JSONDecodeError:
|
||||
log.error(f"Invalid JSON data received: {data}")
|
||||
self._close_connection(sock)
|
||||
return
|
||||
req_type = req.get('type', None)
|
||||
if req_type == 'heartbeat':
|
||||
self._handle_heartbeat(sock, req)
|
||||
elif req_type == 'lic_received_ack':
|
||||
self._handle_lic_received_ack(req)
|
||||
except Exception as e:
|
||||
log.error(f"handle client data error: {e}")
|
||||
self._close_connection(sock)
|
||||
|
||||
def _handle_heartbeat(self, sock, req):
|
||||
""" 心跳包内容
|
||||
{
|
||||
"type":"heartbeat",
|
||||
"user":"",
|
||||
"host":"",
|
||||
"heartbeat_time":datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
"""
|
||||
for item in self.wait_queue:
|
||||
if item['user'] == req['user']:
|
||||
item['sock'] = sock
|
||||
item['heartbeat_time'] = datetime.now()
|
||||
break
|
||||
else:
|
||||
log.warning(f"user {req['user']} not in wait queue")
|
||||
sock.close()
|
||||
return
|
||||
# 如果用户已经分配到license,则直接返回
|
||||
if item['lic'] != '':
|
||||
self.send_lic_dispatch(item)
|
||||
return
|
||||
response = {
|
||||
'type':'heartbeat_ack',
|
||||
'status': 'ok',
|
||||
'heartbeat_time': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
with self.lock:
|
||||
self.message_queues[sock].append(json.dumps(response))
|
||||
if sock not in self.outputs:
|
||||
self.outputs.append(sock)
|
||||
|
||||
def _handle_lic_received_ack(self, req):
|
||||
self._remove_user(req['user'])
|
||||
|
||||
def _handle_client_response(self, sock):
|
||||
try:
|
||||
with self.lock:
|
||||
if sock in self.message_queues and self.message_queues[sock]:
|
||||
next_msg = self.message_queues[sock].pop(0)
|
||||
sock.send(next_msg.encode('utf-8'))
|
||||
else:
|
||||
self.outputs.remove(sock)
|
||||
except Exception as e:
|
||||
log.error(f"send response error: {e}")
|
||||
self._close_connection(sock)
|
||||
|
||||
def _handle_socket_exception(self, sock):
|
||||
log.error(f"handle exception socket: {sock}")
|
||||
self._close_connection(sock)
|
||||
|
||||
def _remove_user(self, user):
|
||||
with self.lock:
|
||||
for idx, item in enumerate(self.wait_queue):
|
||||
if item['user'] != user:
|
||||
continue
|
||||
self.wait_queue.remove(idx)
|
||||
self._close_connection(item['sock'])
|
||||
break
|
||||
|
||||
def _close_connection(self, sock):
|
||||
if not sock:
|
||||
return
|
||||
with self.lock:
|
||||
# 先从所有列表中移除socket
|
||||
if sock in self.outputs:
|
||||
self.outputs.remove(sock)
|
||||
if sock in self.inputs:
|
||||
self.inputs.remove(sock)
|
||||
if sock in self.message_queues:
|
||||
del self.message_queues[sock]
|
||||
try:
|
||||
sock.shutdown(socket.SHUT_RDWR) # 先关闭连接
|
||||
except:
|
||||
pass # 忽略已关闭socket的错误
|
||||
finally:
|
||||
sock.close() # 最后关闭socket
|
||||
|
||||
def _cleanup_inactive_clients(self, timeout=3):
|
||||
"""clean inactive client"""
|
||||
current_time = datetime.now()
|
||||
remove_list = []
|
||||
with self.lock:
|
||||
for idx, item in enumerate(self.wait_queue):
|
||||
if (current_time - item['heartbeat_time']).total_seconds() < timeout:
|
||||
continue
|
||||
log.info(f"clean inactive client: {item['user']}")
|
||||
remove_list.append(item['user'])
|
||||
for user in remove_list:
|
||||
self._remove_user(user)
|
||||
|
||||
def add_wait_queue(self, user, host):
|
||||
data = {
|
||||
"user":user,
|
||||
"host":host,
|
||||
'status':'wait_dispatch',
|
||||
"start_time":datetime.now(),
|
||||
'heartbeat_time':datetime.now(),
|
||||
'lic':'',
|
||||
'sock':None
|
||||
}
|
||||
with self.lock:
|
||||
self.wait_queue.put(data)
|
||||
|
||||
def dispatch_license(self, lic, available_cnt):
|
||||
"""
|
||||
分配license存在几种情况:
|
||||
1. 该用户在等待队列中且没有分配到license,那么就直接分配
|
||||
2. 该用户在等待队列中且已经分配到license,那么就跳过
|
||||
3. 该lic已经被用户占用了,但是这个用户还没有连接,lic处于已经预分配状态,所以也不能再派发给别人
|
||||
"""
|
||||
resp_list = []
|
||||
with self.lock:
|
||||
for item in self.wait_queue:
|
||||
if available_cnt <= 0:
|
||||
break
|
||||
if item['lic'] == lic: # 如果该lic已经被分配了,可用数量减1
|
||||
available_cnt -= 1
|
||||
continue
|
||||
if item['status'] != 'wait_dispatch': # 如果该用户无需进行分配,则跳过
|
||||
continue
|
||||
if item['lic'] != '': # 如果该用户已经分配了,则本次分配跳过
|
||||
continue
|
||||
item['lic'] = lic
|
||||
item['status'] = 'dispatched'
|
||||
resp_list.append(item)
|
||||
available_cnt -= 1
|
||||
for item in resp_list:
|
||||
self.send_lic_dispatch(item)
|
||||
|
||||
def send_lic_dispatch(self, item):
|
||||
sock = item['sock'] #如果sock不存在说明该用户还没连接,等待其连接后再发送lic
|
||||
if not sock:
|
||||
return
|
||||
response = {
|
||||
'user':item['user'],
|
||||
'host':item['host'],
|
||||
'lic':item['lic'],
|
||||
'type':'lic_dispatch',
|
||||
'status': 'ok',
|
||||
'msg':f'{item["user"]} get {item["lic"]} license'
|
||||
}
|
||||
with self.lock:
|
||||
self.message_queues[sock].append(json.dumps(response))
|
||||
if sock not in self.outputs:
|
||||
self.outputs.append(sock)
|
||||
|
||||
|
||||
def _get_ip_address(domain):
|
||||
if MOCK_LMUTIL:
|
||||
return mock_ipaddress(domain)
|
||||
try:
|
||||
ip_address = socket.gethostbyname(domain)
|
||||
# log.debug(f"get_ip_address {domain} {ip_address}")
|
||||
return ip_address
|
||||
except socket.gaierror as e:
|
||||
# print(f"Error retrieving IP address: {e}")
|
||||
log.error(f"get ip address {domain} error: {e}")
|
||||
return None
|
||||
|
||||
def _get_valid_licenses():
|
||||
|
@ -38,20 +269,22 @@ class UsedInfo:
|
|||
self.lic = ''
|
||||
self.start_time:datetime = None
|
||||
def __str__(self):
|
||||
time = datetime.now() - self.start_time
|
||||
login_time = (time.days * 24 + time.seconds / 3600.0)
|
||||
obj = {
|
||||
"user":self.user,
|
||||
"host":self.host,
|
||||
'lic':self.lic,
|
||||
'login':f'{login_time:.2f}h'
|
||||
}
|
||||
obj = self.as_dict()
|
||||
return str(obj)
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
def as_dict(self):
|
||||
time = datetime.now() - self.start_time
|
||||
login_time = (time.days * 24 + time.seconds / 3600.0)
|
||||
return {
|
||||
"user":self.user,
|
||||
"host":self.host,
|
||||
"lic":self.lic,
|
||||
"login":f'{login_time:.2f}h'
|
||||
}
|
||||
|
||||
class LicenseInfo:
|
||||
def __init__(self, name='', total=0, used=0):
|
||||
def __init__(self, name: str = '', total: int = 0, used: int = 0):
|
||||
self.name = name
|
||||
self.total = total
|
||||
self.used = used
|
||||
|
@ -61,66 +294,148 @@ class LicenseInfo:
|
|||
return self.total - self.used
|
||||
|
||||
def __str__(self):
|
||||
obj = {
|
||||
"name":self.name,
|
||||
"total":self.total,
|
||||
"used":self.used,
|
||||
"used_info":self.used_info
|
||||
}
|
||||
obj = self.as_dict()
|
||||
return str(obj)
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def as_dict(self):
|
||||
info = []
|
||||
for used in self.used_info:
|
||||
info.append(used.as_dict())
|
||||
return {
|
||||
"name":self.name,
|
||||
"total":self.total,
|
||||
"used":self.used,
|
||||
"used_info":info
|
||||
}
|
||||
|
||||
class Queue:
|
||||
def __init__(self):
|
||||
self.queue = [] # 使用列表作为底层存储
|
||||
self.lock = threading.Lock() # 创建一个锁
|
||||
|
||||
def put(self, item):
|
||||
with self.lock:
|
||||
self.queue.append(item) # 添加元素到队列
|
||||
|
||||
def pop(self):
|
||||
with self.lock:
|
||||
if not self.is_empty():
|
||||
return self.queue.pop(0) # 从队列头部移除并返回元素
|
||||
else:
|
||||
return None # 如果队列为空,返回None
|
||||
def get(self, idx):
|
||||
with self.lock:
|
||||
if not self.is_empty():
|
||||
return self.queue[idx]
|
||||
else:
|
||||
return None
|
||||
def __getitem__(self, idx):
|
||||
with self.lock:
|
||||
return self.queue[idx]
|
||||
def is_empty(self):
|
||||
with self.lock:
|
||||
return len(self.queue) == 0 # 检查队列是否为空
|
||||
|
||||
def qsize(self):
|
||||
with self.lock:
|
||||
return len(self.queue) # 返回队列大小
|
||||
|
||||
def remove(self, idx):
|
||||
with self.lock:
|
||||
if 0 <= idx < len(self.queue):
|
||||
return self.queue.pop(idx)
|
||||
return None
|
||||
|
||||
class LicenseManager:
|
||||
def __init__(self):
|
||||
self.wait_queue = queue.Queue()
|
||||
self.ack_queue = queue.Queue()
|
||||
self.licenses_stat = LicenseStatistic()
|
||||
self.licenses:Dict[str,LicenseInfo] = {}
|
||||
#启动定时获取
|
||||
self.licenses_stat.lic_info_updater()
|
||||
self.dispatcher = LicenseDispatcher()
|
||||
self.exit_event = threading.Event()
|
||||
self.timer = None
|
||||
self.start_updater() # 替换原来的线程启动
|
||||
|
||||
def lic_info_updater(self):
|
||||
self.update()
|
||||
if self.wait_queue.qsize() > 0:
|
||||
for lic, available_cnt in self.licenses_stat.available():
|
||||
for i in range(available_cnt):
|
||||
req = self.wait_queue.get()
|
||||
if req == None:
|
||||
return
|
||||
req["lic"] = lic
|
||||
self.ack_queue.put(req)
|
||||
threading.Timer(10, self.lic_info_updater).start() #每间隔一段时间更新一次lic的使用情况
|
||||
def start_updater(self):
|
||||
if not self.exit_event.is_set():
|
||||
self.update_once()
|
||||
# 设置下一次执行
|
||||
self.timer = threading.Timer(5.0, self.start_updater)
|
||||
self.timer.start()
|
||||
|
||||
def _in_wait_queue(self, user):
|
||||
return any(user == _.user for _ in list(self.wait_queue))
|
||||
def update_once(self):
|
||||
"""单次更新操作"""
|
||||
self.licenses_stat.update()
|
||||
if self.dispatcher.wait_queue.qsize() > 0:
|
||||
for lic, available_cnt, _ in self.licenses_stat.available():
|
||||
self.dispatcher.dispatch_license(lic, available_cnt)
|
||||
|
||||
def request(self, user:str, host:str):
|
||||
def request(self, user: str, host: str) -> Dict[str, Union[str, dict]]:
|
||||
#如果已经是在线的状态就不允许申请
|
||||
if any(user == _.user for _ in self.licenses_stat.online_users()):
|
||||
log.warning(f"{user} is online, please try again later")
|
||||
return False
|
||||
#如果是已经在队列中则返回等待
|
||||
if self._in_wait_queue(user):
|
||||
return None
|
||||
|
||||
data = {
|
||||
rep = {
|
||||
"user":user,
|
||||
"host":host,
|
||||
"start_time":time.now()
|
||||
"status":"refuse",
|
||||
"lic":'',
|
||||
"msg":f"unknown error"
|
||||
}
|
||||
self.wait_queue.put(data)
|
||||
return False
|
||||
if self.dispatcher.wait_queue.qsize() > 100:
|
||||
rep["status"] = "refuse"
|
||||
rep["msg"] = f"{user} queue is full!!"
|
||||
return rep
|
||||
if user in self.licenses_stat.online_users():
|
||||
rep["msg"] = f"{user} is online"
|
||||
log.warning(rep["msg"])
|
||||
return rep
|
||||
|
||||
def cancel(self):
|
||||
pass
|
||||
|
||||
def release(self):
|
||||
pass
|
||||
#如果是已经在队列中则返回等待
|
||||
item = next((item for item in self.dispatcher.wait_queue if item['user'] == user), None)
|
||||
if item:
|
||||
rep["status"] = "queue"
|
||||
rep["msg"] = f"{user} is in queue!!"
|
||||
rep["start_time"] = item['start_time'].strftime("%Y-%m-%d %H:%M:%S")
|
||||
return rep
|
||||
log.info(f"add {user} to wait queue")
|
||||
# 如果队列为空,则直接分配一个license
|
||||
if self.dispatcher.wait_queue.qsize() == 0:
|
||||
self.dispatcher.add_wait_queue(user, host)
|
||||
lic, available_cnt, _ = next(self.licenses_stat.available(), ('', 0, 0))
|
||||
self.dispatcher.dispatch_license(lic, available_cnt)
|
||||
else:
|
||||
self.dispatcher.add_wait_queue(user, host)
|
||||
rep["status"] = "wait_dispatch"
|
||||
rep["msg"] = f"{user} add to queue"
|
||||
rep["dispatcher_server"] = {
|
||||
"port": DISPATCHER_SERVER_PORT
|
||||
}
|
||||
return rep
|
||||
|
||||
def get_wait_queue(self):
|
||||
return [_ for _ in self.wait_queue.queue]
|
||||
resp = []
|
||||
for item in self.dispatcher.wait_queue:
|
||||
resp.append({
|
||||
"user":item['user'],
|
||||
"host":item['host'],
|
||||
"status":item['status'],
|
||||
"wait_time":int((datetime.now() - item['start_time']).total_seconds())
|
||||
})
|
||||
return resp
|
||||
|
||||
def get_licenses_info(self):
|
||||
resp = {}
|
||||
for lic, info in self.licenses_stat.licenses.items():
|
||||
info = info.as_dict()
|
||||
resp[lic] = info
|
||||
return resp
|
||||
|
||||
def exit(self):
|
||||
self.exit_event.set()
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
self.timer = None
|
||||
if self.dispatcher:
|
||||
self.dispatcher.exit()
|
||||
|
||||
class LicenseStatistic:
|
||||
def __init__(self):
|
||||
|
@ -132,13 +447,23 @@ class LicenseStatistic:
|
|||
for lic in _get_valid_licenses():
|
||||
log.trace(f'find valid lic: {lic}')
|
||||
tasks.append(get_license_used_info(lic))
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
infos = loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
|
||||
for info in infos:
|
||||
if info is not None:
|
||||
if info is None:
|
||||
continue
|
||||
if not isinstance(info, LicenseInfo):
|
||||
log.error(f"get_license_used_info {info} is not LicenseInfo")
|
||||
continue
|
||||
self.licenses[info.name] = info
|
||||
if not self.licenses:
|
||||
log.warning("not vcast lic found")
|
||||
except Exception as e:
|
||||
log.error(f"update license info error: {e}")
|
||||
loop.close()
|
||||
|
||||
def get_lic_list(self):
|
||||
# 获取所有的license名称
|
||||
return self.licenses.keys()
|
||||
|
@ -148,7 +473,7 @@ class LicenseStatistic:
|
|||
for _,lic in self.licenses.items():
|
||||
value = lic.available()
|
||||
if value > 0:
|
||||
yield (lic.name, value) #lic的name和个数
|
||||
yield (lic.name, value, lic.total) #lic的name和个数
|
||||
|
||||
def online_users(self):
|
||||
# 获取在线用户
|
||||
|
@ -179,11 +504,30 @@ class LicenseStatistic:
|
|||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
def shell(cmd, checkret=True):
|
||||
if MOCK_LMUTIL:
|
||||
ret = mock_lmutil(cmd)
|
||||
if not ret:
|
||||
return 1, '', ''
|
||||
return 0, ret, ''
|
||||
process = subprocess.Popen(cmd,
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True)
|
||||
out, err = process.communicate()
|
||||
ret = process.returncode
|
||||
if checkret and ret != 0:
|
||||
raise subprocess.CalledProcessError(ret, cmd, output=out, stderr=err)
|
||||
return ret, out, err
|
||||
|
||||
"""
|
||||
{"27003@szmaslic03": {"name": "27003@szmaslic03", "total": 2, "used": 1, "used_info": "[{"name": "haoxiang.ran", "host": "szl3bc12808", "lic": "szmaslic03", "login": "0.41"}]"}, "27003@szmaslic04": {"name": "27003@szmaslic04", "total": 2, "used": 1, "used_info": "[{"name": "macro.yang", "host": "szl3bc12804", "lic": "szmaslic04", "login": "52.28"}]"}, "27003@szmaslic06": {"name": "27003@szmaslic06", "total": 2, "used": 2, "used_info": "[{"name": "zhenhao.he", "host": "szl3bc12804", "lic": "szmaslic06", "login": "0.20"}, {"name": "phillip.chan", "host": "szl3bc12810", "lic": "szmaslic06", "login": "0.16"}]"}, "27003@szmaslic07": {"name": "27003@szmaslic07", "total": 2, "used": 1, "used_info": "[{"name": "zhiyi.li", "host": "szl3bc12806", "lic": "szmaslic07", "login": "2.96"}]"}, "27003@szmaslic08": {"name": "27003@szmaslic08", "total": 2, "used": 2, "used_info": "[{"name": "soo.liu", "host": "szl3bc12809", "lic": "szmaslic08", "login": "6.55"}, {"name": "haichao.ou", "host": "szl3bc12806", "lic": "szmaslic08", "login": "5.93"}]"}, "27003@szmaslic09": {"name": "27003@szmaslic09", "total": 2, "used": 2, "used_info": "[{"name": "louie.liang", "host": "szl3bc12810", "lic": "szmaslic09", "login": "8.31"}, {"name": "zabbix", "host": "szl3bc06409", "lic": "szmaslic09", "login": "0.90"}]"}, "27003@szmaslic10": {"name": "27003@szmaslic10", "total": 2, "used": 2, "used_info": "[{"name": "harvey.li", "host": "szl3bc12810", "lic": "szmaslic10", "login": "57.38"}, {"name": "kw.hu", "host": "szl3bc12809", "lic": "szmaslic10", "login": "1.41"}]"}}
|
||||
"""
|
||||
async def get_license_used_info(lic):
|
||||
server_used_info = f"timeout 1 /tools/software/vcast/flexlm/lmutil lmstat -a -c {lic} -a"
|
||||
# log.info(f"get_license_used_info {lic}")
|
||||
server_used_info = f"timeout 1 /tools/software/vcast/flexlm/lmutil lmstat -a -c {lic}"
|
||||
ret,out,err = shell(server_used_info, checkret=False)
|
||||
# log.info(f"get_license_used_info {lic} {out} {ret} {err}")
|
||||
if ret != 0:
|
||||
|
@ -218,7 +562,7 @@ async def get_license_used_info(lic):
|
|||
ret = re.findall(pattern, line)
|
||||
if not ret:
|
||||
log.warning(f"{lic} parser [{line}] fail")
|
||||
return None
|
||||
return info
|
||||
(total, used) = ret[0]
|
||||
info.total = int(total)
|
||||
info.used = int(used)
|
||||
|
@ -258,10 +602,7 @@ if __name__ == '__main__':
|
|||
log.info("License status query...")
|
||||
lm = LicenseStatistic()
|
||||
log.info(lm)
|
||||
available_lic = lm.available()
|
||||
if not available_lic:
|
||||
log.info("No license available")
|
||||
log.info(f"All licenses is: {[str(_) for _ in lm.get_lic_list()]}")
|
||||
for lic,number in lm.available():
|
||||
for lic,number,total in lm.available():
|
||||
log.info(f"{lic}: {number} lic available")
|
||||
draw_time_grpah(lm)
|
315
license_client.py
Executable file
315
license_client.py
Executable file
|
@ -0,0 +1,315 @@
|
|||
#!/usr/bin/python3
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
import http.client
|
||||
import sys
|
||||
from my_log import get_logger
|
||||
from mock_lmutil import mock_license_add_user, mock_license_remove_user, _read_sample_file, _write_sample_file
|
||||
import queue
|
||||
import select # 添加 select 模块导入
|
||||
import signal
|
||||
|
||||
|
||||
log = get_logger()
|
||||
|
||||
server = os.environ.get('SSTAR_VCAST_LICENSE_SERVER_HOST') # Use get() to avoid KeyError
|
||||
SERVER_HOST = server if server else '127.0.0.1'
|
||||
SERVER_PORT = 8088
|
||||
SERVER_URL = f'http://{SERVER_HOST}:{SERVER_PORT}'
|
||||
|
||||
class LicenseDispatcherClient:
|
||||
def __init__(self, user: str = '', host: str = '') -> None:
|
||||
self.server_url = SERVER_URL # Use global configuration
|
||||
self.user = user
|
||||
self.host = host
|
||||
self.dispatcher_socket: socket.socket = None
|
||||
self.exit_event = threading.Event()
|
||||
self.lic: queue.Queue = queue.Queue()
|
||||
self.current_lic: str = None
|
||||
self.dispatcher_timer: threading.Timer = None # Add missing attribute
|
||||
self._lock = threading.Lock() # Add thread safety
|
||||
self.heartbeat_thread: threading.Thread | None = None # Add thread reference
|
||||
|
||||
def request(self) -> str:
|
||||
"""Request license, block until license is obtained or user interrupts"""
|
||||
response = self._request_license()
|
||||
if response['status'] == 'refuse':
|
||||
log.error(response['msg'])
|
||||
return None
|
||||
elif response['status'] == 'wait_dispatch':
|
||||
# Start heartbeat thread
|
||||
log.info("Waiting for license allocation...")
|
||||
self._start_dispatcher_client(response['dispatcher_server']['port'])
|
||||
lic = self.get_lic()
|
||||
return lic
|
||||
|
||||
def get_lic(self) -> str:
|
||||
"""Get the assigned license"""
|
||||
self.current_lic = self.lic.get()
|
||||
return self.current_lic
|
||||
|
||||
def _request_license(self) -> dict:
|
||||
"""Send license request to server"""
|
||||
conn = None
|
||||
try:
|
||||
log.info(f"Requesting license: {self.user}@{self.host}")
|
||||
conn = http.client.HTTPConnection(SERVER_HOST, SERVER_PORT, timeout=10) # Add timeout
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
data = {
|
||||
"user": self.user,
|
||||
"host": self.host
|
||||
}
|
||||
conn.request('GET', '/vcast/request_lic',
|
||||
body=json.dumps(data),
|
||||
headers=headers)
|
||||
response = conn.getresponse()
|
||||
response_data = response.read().decode()
|
||||
return json.loads(response_data)
|
||||
except (socket.timeout, ConnectionRefusedError) as e:
|
||||
log.error(f"Connection error: {e}")
|
||||
return {"status": "refuse", "msg": str(e)}
|
||||
except Exception as e:
|
||||
log.error(f"Requesting license error: {e}")
|
||||
return {"status": "error", "msg": str(e)}
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
def _start_dispatcher_client(self, port: int) -> None:
|
||||
"""Start dispatcher connection with server"""
|
||||
log.info(f"Starting dispatcher: {port}")
|
||||
self.dispatcher_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.dispatcher_socket.connect((SERVER_HOST, port))
|
||||
start_time = time.time()
|
||||
|
||||
def heartbeat_loop():
|
||||
while not self.exit_event.is_set():
|
||||
try:
|
||||
# Send heartbeat
|
||||
heartbeat = {
|
||||
"type": "heartbeat",
|
||||
"user": self.user,
|
||||
"host": self.host,
|
||||
"heartbeat_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
self.dispatcher_socket.send(json.dumps(heartbeat).encode('utf-8'))
|
||||
|
||||
# Wait for response with timeout
|
||||
readable, _, _ = select.select([self.dispatcher_socket], [], [], 0.5)
|
||||
|
||||
if self.dispatcher_socket in readable:
|
||||
data = self.dispatcher_socket.recv(1024)
|
||||
if not data:
|
||||
log.warning("Connection closed by server")
|
||||
break
|
||||
|
||||
response = json.loads(data.decode())
|
||||
if response.get('type') == 'lic_dispatch':
|
||||
self.lic.put(response['lic'])
|
||||
log.info(f"Received license: {response['lic']}")
|
||||
# Send acknowledgment
|
||||
ack = {
|
||||
"type": "lic_received_ack",
|
||||
"user": self.user,
|
||||
"host": self.host
|
||||
}
|
||||
self.dispatcher_socket.send(json.dumps(ack).encode('utf-8'))
|
||||
break
|
||||
elif response.get('type') == 'heartbeat_ack':
|
||||
wait_time = int(time.time() - start_time)
|
||||
print(f"\rwaiting... {wait_time}s", end='', flush=True)
|
||||
elif response.get('type') == 'queue':
|
||||
wait_time = int(time.time() - start_time)
|
||||
log.info(f"waiting... {wait_time}s")
|
||||
|
||||
time.sleep(0.5) # Small sleep to prevent CPU spinning
|
||||
except Exception as e:
|
||||
log.error(f"Error in heartbeat loop: {e}")
|
||||
break
|
||||
|
||||
self.lic.put(None) # Signal end of heartbeat loop
|
||||
|
||||
# Start heartbeat thread
|
||||
self.heartbeat_thread = threading.Thread(target=heartbeat_loop, daemon=True)
|
||||
self.heartbeat_thread.start()
|
||||
|
||||
def release(self) -> None:
|
||||
"""Release the current license"""
|
||||
log.info("License released")
|
||||
|
||||
def exit(self) -> None:
|
||||
"""Exit the client and cleanup resources"""
|
||||
try:
|
||||
self.exit_event.set()
|
||||
|
||||
# Wait for heartbeat thread to finish with timeout
|
||||
if self.heartbeat_thread and self.heartbeat_thread.is_alive():
|
||||
self.heartbeat_thread.join(timeout=1.0)
|
||||
|
||||
with self._lock:
|
||||
if self.dispatcher_socket:
|
||||
try:
|
||||
self.dispatcher_socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception:
|
||||
pass
|
||||
self.dispatcher_socket.close()
|
||||
self.dispatcher_socket = None
|
||||
|
||||
self.release()
|
||||
self.lic.put(None)
|
||||
except Exception as e:
|
||||
log.error(f"Error during exit: {e}")
|
||||
|
||||
# Global flag for exit control
|
||||
is_exiting = False
|
||||
clients: dict[str, LicenseDispatcherClient] = {}
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
"""Handle interrupt signals"""
|
||||
global is_exiting
|
||||
if is_exiting:
|
||||
log.info("\nForce exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
log.info("\nReceived interrupt signal, cleaning up...")
|
||||
is_exiting = True
|
||||
cleanup_and_exit()
|
||||
|
||||
def cleanup_and_exit():
|
||||
"""Cleanup all resources and exit"""
|
||||
for client_key, client in list(clients.items()):
|
||||
try:
|
||||
log.info(f"Cleaning up client: {client_key}")
|
||||
client.exit()
|
||||
del clients[client_key]
|
||||
except Exception as e:
|
||||
log.error(f"Error cleaning up client {client_key}: {e}")
|
||||
log.info("\nProgram exited")
|
||||
sys.exit(0)
|
||||
|
||||
def main() -> None:
|
||||
# Register signal handlers
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
# Make sure SIGINT is not ignored
|
||||
if os.name == 'posix':
|
||||
signal.siginterrupt(signal.SIGINT, True)
|
||||
|
||||
while not is_exiting:
|
||||
show_menu()
|
||||
# Set timeout for input operations
|
||||
cmd = input("Please select an operation: ").strip()
|
||||
|
||||
if is_exiting: # Check if exit flag was set during input
|
||||
break
|
||||
|
||||
if cmd == '1':
|
||||
create_client()
|
||||
elif cmd == '2':
|
||||
release_client()
|
||||
elif cmd == '3':
|
||||
show_clients()
|
||||
elif cmd == '4':
|
||||
break
|
||||
else:
|
||||
print("Invalid command")
|
||||
if not is_exiting:
|
||||
cleanup_and_exit()
|
||||
|
||||
def create_client():
|
||||
"""Create new license client"""
|
||||
global is_exiting
|
||||
username = input("Please enter username: ").strip()
|
||||
hostname = input("Please enter hostname: ").strip()
|
||||
if is_exiting:
|
||||
return
|
||||
|
||||
if not username or not hostname:
|
||||
print("Username and hostname cannot be empty")
|
||||
return
|
||||
|
||||
client_key = f"{username}@{hostname}"
|
||||
if client_key in clients:
|
||||
print(f"Client {client_key} already exists")
|
||||
return
|
||||
|
||||
client = LicenseDispatcherClient(user=username, host=hostname)
|
||||
lic = client.request()
|
||||
if lic:
|
||||
clients[client_key] = client
|
||||
# Add mock license here
|
||||
mock_license_add_user(lic, username, hostname)
|
||||
print(f"{client_key} successfully created and requested license: {lic}")
|
||||
else:
|
||||
print(f"{client_key} License request failed")
|
||||
|
||||
def release_client():
|
||||
"""Release existing license"""
|
||||
lic_use_info = _read_sample_file()
|
||||
if not lic_use_info:
|
||||
print("No license usage record currently")
|
||||
return
|
||||
|
||||
print("\nCurrent license usage:")
|
||||
users_list = []
|
||||
|
||||
# Iterate through all license server information
|
||||
for server_info in lic_use_info.values():
|
||||
for used_info in server_info.get("used_info", []):
|
||||
user = used_info["user"]
|
||||
host = used_info["host"]
|
||||
lic = used_info["lic"]
|
||||
users_list.append((user, host, lic))
|
||||
print(f"{len(users_list)}: user: {user}, host: {host}, lic: {lic}")
|
||||
|
||||
if not users_list:
|
||||
print("No valid user records found")
|
||||
return
|
||||
|
||||
try:
|
||||
choice = int(input("\nPlease select the license number to release (0 to cancel): "))
|
||||
except ValueError:
|
||||
print("Please enter a valid number")
|
||||
if choice == 0:
|
||||
return
|
||||
if 1 <= choice <= len(users_list):
|
||||
user, host, lic = users_list[choice-1]
|
||||
client_key = f"{user}@{host}"
|
||||
lic_use_info = _read_sample_file()
|
||||
|
||||
# If the client exists in the clients dictionary, release it
|
||||
if client_key in clients:
|
||||
client = clients[client_key]
|
||||
client.release()
|
||||
client.exit()
|
||||
del clients[client_key]
|
||||
|
||||
# Remove mock license record
|
||||
mock_license_remove_user(user, host)
|
||||
print(f"Released license: user: {user}, host: {host}, lic: {lic}")
|
||||
else:
|
||||
print("Invalid selection")
|
||||
|
||||
def show_clients():
|
||||
if not clients:
|
||||
print("No active clients currently")
|
||||
return
|
||||
|
||||
print("\nCurrent active clients:")
|
||||
for key, client in clients.items():
|
||||
lic_status = "License obtained" if client.current_lic else "Waiting"
|
||||
print(f"- {key}: {lic_status}")
|
||||
|
||||
def show_menu() -> None:
|
||||
print("\n=== License Management System ===")
|
||||
print("1: Create new license request")
|
||||
print("2: Release license")
|
||||
print("3: Show current clients")
|
||||
print("4: Exit")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
200
mock_lmutil.py
Executable file
200
mock_lmutil.py
Executable file
|
@ -0,0 +1,200 @@
|
|||
from datetime import datetime
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
|
||||
global MOCK_LIC_USE_INFO
|
||||
MOCK_LIC_USE_INFO = {
|
||||
"27003@szmaslic03": {
|
||||
"name": "27003@szmaslic03",
|
||||
"total": 2,
|
||||
"used": 1,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "haoxiang.ran",
|
||||
"host": "szl3bc12808",
|
||||
"lic": "szmaslic03",
|
||||
"login": "0.41h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic04": {
|
||||
"name": "27003@szmaslic04",
|
||||
"total": 2,
|
||||
"used": 1,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "macro.yang",
|
||||
"host": "szl3bc12804",
|
||||
"lic": "szmaslic04",
|
||||
"login": "52.28h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic06": {
|
||||
"name": "27003@szmaslic06",
|
||||
"total": 2,
|
||||
"used": 2,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "zhenhao.he",
|
||||
"host": "szl3bc12804",
|
||||
"lic": "szmaslic06",
|
||||
"login": "0.20h"
|
||||
},
|
||||
{
|
||||
"user": "phillip.chan",
|
||||
"host": "szl3bc12810",
|
||||
"lic": "szmaslic06",
|
||||
"login": "0.16h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic07": {
|
||||
"name": "27003@szmaslic07",
|
||||
"total": 2,
|
||||
"used": 1,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "zhiyi.li",
|
||||
"host": "szl3bc12806",
|
||||
"lic": "szmaslic07",
|
||||
"login": "2.96h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic08": {
|
||||
"name": "27003@szmaslic08",
|
||||
"total": 2,
|
||||
"used": 2,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "soo.liu",
|
||||
"host": "szl3bc12809",
|
||||
"lic": "szmaslic08",
|
||||
"login": "6.55h"
|
||||
},
|
||||
{
|
||||
"user": "haichao.ou",
|
||||
"host": "szl3bc12806",
|
||||
"lic": "szmaslic08",
|
||||
"login": "5.93h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic09": {
|
||||
"name": "27003@szmaslic09",
|
||||
"total": 2,
|
||||
"used": 2,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "louie.liang",
|
||||
"host": "szl3bc12810",
|
||||
"lic": "szmaslic09",
|
||||
"login": "8.31h"
|
||||
},
|
||||
{
|
||||
"user": "zabbix",
|
||||
"host": "szl3bc06409",
|
||||
"lic": "szmaslic09",
|
||||
"login": "0.90h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic10": {
|
||||
"name": "27003@szmaslic10",
|
||||
"total": 2,
|
||||
"used": 2,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "harvey.li",
|
||||
"host": "szl3bc12810",
|
||||
"lic": "szmaslic10",
|
||||
"login": "57.38h"
|
||||
},
|
||||
{
|
||||
"user": "kw.hu",
|
||||
"host": "szl3bc12809",
|
||||
"lic": "szmaslic10",
|
||||
"login": "1.41h"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def _read_sample_file(file_name:str='lic_info.json'):
|
||||
with open(file_name, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def _write_sample_file(lic_use_info, file_name:str='lic_info.json'):
|
||||
with open(file_name, 'w') as f:
|
||||
json.dump(lic_use_info, f, indent=4)
|
||||
|
||||
def mock_ipaddress(domain):
|
||||
id = int(domain[-2:])
|
||||
if id > 10:
|
||||
return None
|
||||
return f"192.168.1.{id}"
|
||||
|
||||
def mock_lmutil(cmd):
|
||||
lic_name = cmd.split(" ")[-1]
|
||||
lic_use_info = _read_sample_file()
|
||||
info = lic_use_info.get(lic_name, None)
|
||||
if info is None:
|
||||
return ''
|
||||
used_template = " {user} {host} /dev/tty (v23) ({lic}/27003 337), start {start_time}\n"
|
||||
template = """
|
||||
Users of VCAST_C_ENT_0800: (Total of {total} licenses issued; Total of {used} licenses in use)
|
||||
|
||||
"VCAST_C_ENT_0800" v23, vendor: vector, expiry: permanent(no expiration date)
|
||||
vendor_string: CUST:64925:
|
||||
floating license
|
||||
|
||||
{used_info}
|
||||
|
||||
Users of CCAST_0801: (Total of 2 licenses issued; Total of 0 licenses in use)
|
||||
"""
|
||||
used_info = ''
|
||||
for item in info.get("used_info", []):
|
||||
start_time = time.time() - float(item.get("login")[:-1]) * 3600
|
||||
start_time = datetime.fromtimestamp(start_time)
|
||||
start_time = start_time.strftime("%a %m/%d %H:%M")
|
||||
used_info += used_template.format(user=item.get("user"), host=item.get("host"), lic=item.get("lic"), start_time=start_time)
|
||||
ret = template.format(total=info.get("total"), used=info.get("used"), used_info=used_info)
|
||||
return ret
|
||||
|
||||
|
||||
def mock_license_add_user(lic, user, host):
|
||||
key = lic
|
||||
lic_use_info = _read_sample_file()
|
||||
temp = lic_use_info.get(key, None)
|
||||
used_info = {
|
||||
"user": user,
|
||||
"host": host,
|
||||
"lic": lic,
|
||||
"login": f"{random.randint(1, 1000)/ 100.0:.2f}h"
|
||||
}
|
||||
if temp is None:
|
||||
lic_use_info[key] = {
|
||||
"name": key,
|
||||
"total": 2,
|
||||
"used": 0,
|
||||
"used_info": [used_info]
|
||||
}
|
||||
else:
|
||||
lic_use_info[key]["used"] += 1
|
||||
lic_use_info[key]["used_info"].append(used_info)
|
||||
_write_sample_file(lic_use_info)
|
||||
|
||||
def mock_license_remove_user(user, host):
|
||||
lic_use_info = _read_sample_file()
|
||||
for _, info in lic_use_info.items():
|
||||
for item in info.get("used_info", []):
|
||||
if item.get("user") == user and item.get("host") == host:
|
||||
info["used"] -= 1
|
||||
info["used_info"].remove(item)
|
||||
_write_sample_file(lic_use_info)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_write_sample_file(MOCK_LIC_USE_INFO)
|
55
my_log.py
Executable file
55
my_log.py
Executable file
|
@ -0,0 +1,55 @@
|
|||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
# 定义日志级别
|
||||
TRACE = 5 # 自定义TRACE级别
|
||||
logging.addLevelName(TRACE, 'TRACE')
|
||||
|
||||
class CustomLogger(logging.Logger):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
|
||||
def trace(self, msg, *args, **kwargs):
|
||||
"""添加TRACE级别的日志方法"""
|
||||
if self.isEnabledFor(TRACE):
|
||||
self._log(TRACE, msg, args, **kwargs)
|
||||
|
||||
def get_logger(name='vcast_license'):
|
||||
"""获取自定义logger实例"""
|
||||
# 注册自定义Logger类
|
||||
logging.setLoggerClass(CustomLogger)
|
||||
|
||||
# 创建logger
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# 如果logger已经有处理器,直接返回
|
||||
if logger.handlers:
|
||||
return logger
|
||||
|
||||
# 创建控制台处理器
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(logging.DEBUG)
|
||||
|
||||
# 创建格式化器
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s [%(levelname)s] [%(name)s:%(lineno)d] %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
# 添加处理器到logger
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
return logger
|
||||
|
||||
# 测试代码
|
||||
if __name__ == '__main__':
|
||||
log = get_logger()
|
||||
log.trace('这是一条TRACE日志')
|
||||
log.debug('这是一条DEBUG日志')
|
||||
log.info('这是一条INFO日志')
|
||||
log.warning('这是一条WARNING日志')
|
||||
log.error('这是一条ERROR日志')
|
||||
log.critical('这是一条CRITICAL日志')
|
201
server.py
201
server.py
|
@ -1,139 +1,33 @@
|
|||
#!/usr/bin/python3
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from license import LicenseManager
|
||||
from my_log import get_logger
|
||||
log = get_logger()
|
||||
|
||||
sample_data = {
|
||||
"27003@szmaslic03": {
|
||||
"name": "27003@szmaslic03",
|
||||
"total": 2,
|
||||
"used": 1,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "haoxiang.ran",
|
||||
"host": "szl3bc12808",
|
||||
"lic": "szmaslic03",
|
||||
"login": "0.41h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic04": {
|
||||
"name": "27003@szmaslic04",
|
||||
"total": 2,
|
||||
"used": 1,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "macro.yang",
|
||||
"host": "szl3bc12804",
|
||||
"lic": "szmaslic04",
|
||||
"login": "52.28h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic06": {
|
||||
"name": "27003@szmaslic06",
|
||||
"total": 2,
|
||||
"used": 2,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "zhenhao.he",
|
||||
"host": "szl3bc12804",
|
||||
"lic": "szmaslic06",
|
||||
"login": "0.20h"
|
||||
},
|
||||
{
|
||||
"user": "phillip.chan",
|
||||
"host": "szl3bc12810",
|
||||
"lic": "szmaslic06",
|
||||
"login": "0.16h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic07": {
|
||||
"name": "27003@szmaslic07",
|
||||
"total": 2,
|
||||
"used": 1,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "zhiyi.li",
|
||||
"host": "szl3bc12806",
|
||||
"lic": "szmaslic07",
|
||||
"login": "2.96h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic08": {
|
||||
"name": "27003@szmaslic08",
|
||||
"total": 2,
|
||||
"used": 2,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "soo.liu",
|
||||
"host": "szl3bc12809",
|
||||
"lic": "szmaslic08",
|
||||
"login": "6.55h"
|
||||
},
|
||||
{
|
||||
"user": "haichao.ou",
|
||||
"host": "szl3bc12806",
|
||||
"lic": "szmaslic08",
|
||||
"login": "5.93h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic09": {
|
||||
"name": "27003@szmaslic09",
|
||||
"total": 2,
|
||||
"used": 2,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "louie.liang",
|
||||
"host": "szl3bc12810",
|
||||
"lic": "szmaslic09",
|
||||
"login": "8.31h"
|
||||
},
|
||||
{
|
||||
"user": "zabbix",
|
||||
"host": "szl3bc06409",
|
||||
"lic": "szmaslic09",
|
||||
"login": "0.90h"
|
||||
}
|
||||
]
|
||||
},
|
||||
"27003@szmaslic10": {
|
||||
"name": "27003@szmaslic10",
|
||||
"total": 2,
|
||||
"used": 2,
|
||||
"used_info": [
|
||||
{
|
||||
"user": "harvey.li",
|
||||
"host": "szl3bc12810",
|
||||
"lic": "szmaslic10",
|
||||
"login": "57.38h"
|
||||
},
|
||||
{
|
||||
"user": "kw.hu",
|
||||
"host": "szl3bc12809",
|
||||
"lic": "szmaslic10",
|
||||
"login": "1.41h"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
global LIC_MANAGER
|
||||
LIC_MANAGER = None
|
||||
|
||||
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.handler_map = {
|
||||
'/vcast/lic_use_info': self.send_lic_info,
|
||||
'/vcast/request_lic': self.send_request_lic,
|
||||
'/vcast/queue_info': self.send_queue_info,
|
||||
}
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
return # 重写该方法以禁用日志输出
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == '/vcast/lic_use_info':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/json')
|
||||
self.end_headers()
|
||||
data = json.dumps(sample_data)
|
||||
self.wfile.write(data.encode('utf-8'))
|
||||
if self.path in self.handler_map:
|
||||
self.handler_map[self.path]()
|
||||
self.wfile.flush()
|
||||
self.close_connection = True
|
||||
return
|
||||
path = self.path
|
||||
if path == '/':
|
||||
|
@ -141,16 +35,60 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
|||
path = path.strip('/')
|
||||
if not self.send_file(path):
|
||||
self.send_404()
|
||||
self.wfile.flush()
|
||||
self.close_connection = True
|
||||
|
||||
def send_lic_info(self):
|
||||
data = LIC_MANAGER.get_licenses_info()
|
||||
resp_str = json.dumps(data).encode('utf-8')
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/json')
|
||||
self.send_header('Content-Length', len(resp_str))
|
||||
self.end_headers()
|
||||
self.wfile.write(resp_str)
|
||||
self.wfile.flush()
|
||||
self.close_connection = True
|
||||
|
||||
def send_queue_info(self):
|
||||
data = LIC_MANAGER.get_wait_queue()
|
||||
resp_str = json.dumps(data).encode('utf-8')
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/json')
|
||||
self.send_header('Content-Length', len(resp_str))
|
||||
self.end_headers()
|
||||
self.wfile.write(resp_str)
|
||||
self.wfile.flush()
|
||||
self.close_connection = True
|
||||
|
||||
def send_request_lic(self):
|
||||
content_length = int(self.headers.get('Content-Length'))
|
||||
data = self.rfile.read(content_length)
|
||||
data = json.loads(data)
|
||||
log.info(f"request license: {data['user']}@{data['host']}")
|
||||
resp = LIC_MANAGER.request(data["user"], data["host"])
|
||||
resp_str = json.dumps(resp).encode('utf-8')
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/json')
|
||||
self.send_header('Content-Length', len(resp_str))
|
||||
self.end_headers()
|
||||
self.wfile.write(resp_str)
|
||||
self.wfile.flush()
|
||||
self.close_connection = True
|
||||
|
||||
def send_404(self):
|
||||
self.send_response(404)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
resp_str = "<h1>404 Not Found</h1><p>The page you are looking for does not exist.</p>".encode('utf-8')
|
||||
self.send_header('Content-Length', len(resp_str))
|
||||
self.end_headers()
|
||||
self.wfile.write(b"<h1>404 Not Found</h1><p>The page you are looking for does not exist.</p>")
|
||||
self.wfile.write(resp_str)
|
||||
self.wfile.flush()
|
||||
self.close_connection = True
|
||||
|
||||
|
||||
def send_file(self, file_path) -> bool:
|
||||
file_path =str(Path(os.path.abspath(__file__)).parent.joinpath(file_path))
|
||||
print(f"clinet request: [{file_path}]")
|
||||
log.info(f"clinet request: [{file_path}]")
|
||||
if not os.path.isfile(file_path):
|
||||
return False
|
||||
self.send_response(200)
|
||||
|
@ -177,14 +115,21 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
|||
return 'text/css'
|
||||
# 添加更多 MIME 类型根据需要
|
||||
return 'application/octet-stream' # 默认类型
|
||||
def run(server_class=HTTPServer, handler_class=SimpleHTTPRequestHandler, port=8088):
|
||||
def run(server_class=ThreadingHTTPServer, handler_class=SimpleHTTPRequestHandler, port=8088):
|
||||
server_address = ('0.0.0.0', port)
|
||||
httpd = server_class(server_address, handler_class)
|
||||
print(f'Starting server on {server_address[0]}:{server_address[1]} ...')
|
||||
log.info(f'Starting threaded server on http://{server_address[0]}:{server_address[1]} ...')
|
||||
httpd.serve_forever()
|
||||
|
||||
if __name__ == "__main__":
|
||||
LIC_MANAGER = LicenseManager()
|
||||
try:
|
||||
if len(os.sys.argv) > 1:
|
||||
run(port=int(os.sys.argv[1]))
|
||||
else:
|
||||
run()
|
||||
except KeyboardInterrupt:
|
||||
log.info("Keyboard interrupt received, shutting down...")
|
||||
finally:
|
||||
LIC_MANAGER.exit()
|
||||
log.info("Server stopped")
|
||||
|
|
|
@ -16,12 +16,10 @@ export function createLicenseBar(license, max_duration) {
|
|||
// Create bars for each slot
|
||||
for (let i = 0; i < license.totalSlots; i++) {
|
||||
const usage = license.usageDetails[i];
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'bar-container';
|
||||
|
||||
const barContainer = document.createElement('div');
|
||||
barContainer.className = 'bar-wrapper';
|
||||
|
||||
const barWrapper = document.createElement('div');
|
||||
barWrapper.className = 'bar-wrapper';
|
||||
console.log(usage)
|
||||
|
||||
if (!usage) {
|
||||
const bar = document.createElement('div');
|
||||
|
@ -34,77 +32,51 @@ export function createLicenseBar(license, max_duration) {
|
|||
requestAnimationFrame(() => {
|
||||
label.style.transform = 'translate(0%)';
|
||||
});
|
||||
barWrapper.appendChild(bar);
|
||||
} else {
|
||||
const barContainer = document.createElement('div');
|
||||
barContainer.className = 'bar-container';
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'bar';
|
||||
const width = usage.duration / max_duration * 100;
|
||||
// 创建标签容器
|
||||
const labelContainer = document.createElement('div');
|
||||
labelContainer.className = 'label-container';
|
||||
// const labelContainer = document.createElement('div');
|
||||
// labelContainer.className = 'label-container';
|
||||
|
||||
// 用户信息标签
|
||||
const userLabel = document.createElement('div');
|
||||
userLabel.className = 'user-label';
|
||||
userLabel.textContent = usage.userName;
|
||||
|
||||
const blankDiv = document.createElement('div');
|
||||
blankDiv.className = 'blank-div';
|
||||
blankDiv.textContent = ' ';
|
||||
|
||||
// 时长标签
|
||||
const durationLabel = document.createElement('div');
|
||||
durationLabel.className = 'duration-label';
|
||||
durationLabel.textContent = `${usage.duration.toFixed(1)}h`;
|
||||
|
||||
if (width < 50) {
|
||||
labelContainer.appendChild(durationLabel);
|
||||
bar.appendChild(userLabel);
|
||||
} else {
|
||||
bar.appendChild(userLabel);
|
||||
labelContainer.appendChild(durationLabel);
|
||||
}
|
||||
// const label = document.createElement('div');
|
||||
// label.className = 'bar-label';
|
||||
// label.textContent = `${usage.userName} (${usage.duration}h)`;
|
||||
// // const hostLabel = document.createElement('div');
|
||||
// // hostLabel.className = 'user_info-label';
|
||||
// // hostLabel.textContent = ``;
|
||||
// // // hostLabel.textContent = `${usage.userName}@${usage.hostName}`;
|
||||
bar.appendChild(blankDiv);
|
||||
bar.appendChild(durationLabel);
|
||||
|
||||
// // barElement.appendChild(hostLabel);
|
||||
// barElement.appendChild(label);
|
||||
// 将标签添加到bar容器中
|
||||
barContainer.appendChild(labelContainer);
|
||||
barContainer.appendChild(bar);
|
||||
barWrapper.appendChild(bar);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const user_width = userLabel.offsetWidth;
|
||||
const duration_width = labelContainer.offsetWidth
|
||||
const duration_per = duration_width / bar.offsetWidth;
|
||||
bar.style.width = `${width}%`;
|
||||
|
||||
// 根据宽度决定标签位置
|
||||
if (width + duration_per >= 100) { // 如果超出bar的范围则放在里面
|
||||
labelContainer.classList.add('inside');
|
||||
} else {
|
||||
labelContainer.classList.remove('outside');
|
||||
labelContainer.classList.add('outside');
|
||||
// const per = user_width * 100 / bar.offsetWidth;
|
||||
const rect1 = userLabel.getBoundingClientRect();
|
||||
const rect2 = durationLabel.getBoundingClientRect();
|
||||
|
||||
// 检查重叠
|
||||
const isOverlapping = !(rect1.right < rect2.left ||
|
||||
rect1.left > rect2.right);
|
||||
|
||||
if (isOverlapping) {
|
||||
// 如果重叠,将 element2 移动到 element1 的右侧
|
||||
console.log("trace", rect1.right)
|
||||
labelContainer.style.left = `${userLabel.offsetLeft + userLabel.offsetWidth + 10}px`; // 10px 的间距
|
||||
} else {
|
||||
labelContainer.style.left = `${width + max_duration / 200}%`
|
||||
}
|
||||
const barWidth = barWrapper.offsetWidth * width / 100;
|
||||
const userLabelWidth = userLabel.offsetWidth;
|
||||
const durationLabelWidth = durationLabel.offsetWidth;
|
||||
if (userLabelWidth + durationLabelWidth > barWidth) {
|
||||
const newMargin = Math.max(8, barWidth - userLabelWidth);
|
||||
blankDiv.style.width = `${newMargin - 8}px`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
barsContainer.appendChild(barContainer);
|
||||
barsContainer.appendChild(barWrapper);
|
||||
}
|
||||
|
||||
container.appendChild(nameSection);
|
||||
|
|
|
@ -20,10 +20,18 @@ export function createQueueDisplay(queueData) {
|
|||
|
||||
queueData.forEach(item => {
|
||||
const queueItem = document.createElement('div');
|
||||
let wait_time = item.wait_time
|
||||
if (wait_time < 60) {
|
||||
wait_time = `${Math.round(wait_time)}秒`
|
||||
} else if (wait_time < 3600) {
|
||||
wait_time = `${Math.round(wait_time / 60)}分钟`
|
||||
} else {
|
||||
wait_time = `${Math.round(wait_time / 3600)}小时${Math.round(wait_time % 3600 / 60)}分钟`
|
||||
}
|
||||
queueItem.className = 'queue-item';
|
||||
queueItem.innerHTML = `
|
||||
<span class="queue-user">${item.userName}</span>
|
||||
<span class="queue-time">${item.waitTime}</span>
|
||||
<span class="queue-user">${item.user}</span>
|
||||
<span class="queue-time">${wait_time}</span>
|
||||
`;
|
||||
queueList.appendChild(queueItem);
|
||||
});
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
// 测试用的模拟队列数据
|
||||
const mockQueueData = [
|
||||
{ userName: "Alice Chen", waitTime: "15分钟" },
|
||||
{ userName: "Bob Wang", waitTime: "8分钟" },
|
||||
{ userName: "Charlie Liu", waitTime: "3分钟" }
|
||||
{ user: "Alice Chen", wait_time: "15" },
|
||||
{ user: "Bob Wang", wait_time: "1300" },
|
||||
{ user: "Charlie Liu", wait_time: "1200" }
|
||||
];
|
||||
|
||||
export async function fetchQueueData() {
|
||||
try {
|
||||
const response = await fetch('/vcast/queue_info');
|
||||
const data = await response.json();
|
||||
// console.log(data)
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching queue data:', error);
|
||||
|
|
68
style.css
68
style.css
|
@ -46,39 +46,28 @@ h1 {
|
|||
}
|
||||
|
||||
.slot-info {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.usage-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 60px;
|
||||
gap: 6px;
|
||||
flex-grow: 1;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.bar-wrapper {
|
||||
position: relative;
|
||||
height: 32px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
overflow: visible;
|
||||
/* 允许标签显示在外部 */
|
||||
}
|
||||
|
||||
.bar-container {
|
||||
flex: 1;
|
||||
background-color: #eee;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar-wrapper {
|
||||
display: flex;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 100%;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background-color: #4caf50;
|
||||
display: flex;
|
||||
|
@ -96,9 +85,7 @@ h1 {
|
|||
}
|
||||
|
||||
.label-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 100%;
|
||||
transition: all 0.3s ease;
|
||||
|
@ -106,34 +93,42 @@ h1 {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.label-container.outside {
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.label-container.inside {
|
||||
right: 8px;
|
||||
color: #fff;
|
||||
/* 一个空白的div */
|
||||
.blank-div {
|
||||
height: 100%;
|
||||
background-color: transparent;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
color: #212121;
|
||||
font-size: 16px;
|
||||
white-space: nowrap;
|
||||
margin: 10px;
|
||||
margin-left: 10%;
|
||||
margin: 6px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-label {
|
||||
margin: 10px;
|
||||
margin: 8px;
|
||||
text-align: center;
|
||||
color: rgba(33, 33, 33, 0.7);
|
||||
font-size: 14px;
|
||||
display: inline;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.duration-label {
|
||||
font-size: 12px;
|
||||
margin: 6px;
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
display: inline;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.free .bar-label {
|
||||
|
@ -173,6 +168,7 @@ h1 {
|
|||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.queue-title {
|
||||
|
|
Loading…
Reference in New Issue
Block a user