vcast_license_usage_server/license.py
2025-01-13 08:46:49 +08:00

609 lines
22 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/python3
import json
import re
import socket
import asyncio
import subprocess
import threading
import time
from typing import Dict, List, Optional, Union
from datetime import datetime
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)
return ip_address
except socket.gaierror as e:
log.error(f"get ip address {domain} error: {e}")
return None
def _get_valid_licenses():
lic_id = 0
while True:
lic_id += 1
domain = f"szmaslic{lic_id:02d}"
if _get_ip_address(domain) is None:
break
yield "27003@" + domain
class UsedInfo:
def __init__(self):
self.user = ''
self.host = ''
self.lic = ''
self.start_time:datetime = None
def __str__(self):
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: str = '', total: int = 0, used: int = 0):
self.name = name
self.total = total
self.used = used
self.used_info:List[UsedInfo] = []
def available(self):
return self.total - self.used
def __str__(self):
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.licenses_stat = LicenseStatistic()
self.licenses:Dict[str,LicenseInfo] = {}
self.dispatcher = LicenseDispatcher()
self.exit_event = threading.Event()
self.timer = None
self.start_updater() # 替换原来的线程启动
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 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) -> Dict[str, Union[str, dict]]:
#如果已经是在线的状态就不允许申请
rep = {
"user":user,
"host":host,
"status":"refuse",
"lic":'',
"msg":f"unknown error"
}
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
#如果是已经在队列中则返回等待
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):
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):
self.licenses:Dict[str,LicenseInfo] = {}
def update(self):
# 更新license信息使用异步获取
tasks = []
for lic in _get_valid_licenses():
log.trace(f'find valid lic: {lic}')
tasks.append(get_license_used_info(lic))
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 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()
def available(self):
# 获取所有的可用数量
for _,lic in self.licenses.items():
value = lic.available()
if value > 0:
yield (lic.name, value, lic.total) #lic的name和个数
def online_users(self):
# 获取在线用户
for used_info in self.used():
yield used_info.user #返回的是user name
def used(self):
# 获取所有的已使用信息
for _,info in self.licenses.items():
for used in info.used_info:
yield used #返回的是UsedInfo对象
def sorted(self):
info = {}
for used_info in self.used():
time = datetime.now() - used_info.start_time
login_time = (time.days * 24 + time.seconds / 3600.0)
info[used_info.user] = login_time
sorted_info = dict(sorted(info.items(), key=lambda item: item[1], reverse=True))
return sorted_info #返回的是字典key为username value为登录时间
def __str__(self):
string = str(self.licenses).replace("'", '"')
log.info(string)
obj = json.loads(string)
string = json.dumps(obj, indent = 4)
return string
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):
# 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:
return None
""" data like:
Users of VCAST_C_ENT_0800: (Total of 2 licenses issued; Total of 2 licenses in use)
"VCAST_C_ENT_0800" v23, vendor: vector, expiry: permanent(no expiration date)
vendor_string: CUST:64925:
floating license
zhenhao.he szl3bc12804 /dev/tty (v23) (szmaslic06/27003 337), start Thu 1/2 8:52
phillip.chan szl3bc12810 /dev/tty (v23) (szmaslic06/27003 233), start Thu 1/2 14:59
Users of CCAST_0801: (Total of 2 licenses issued; Total of 0 licenses in use)
"""
status = 'idle'
info = LicenseInfo(lic)
for line in out.splitlines():
line = line.strip()
if status == 'idle':
#Error getting status: Cannot find license file. (-1,359:2 "No such file or directory")
if line.startswith("Error getting status"):
log.warning(f"{lic}")
return None
if not line.startswith("Users of VCAST_C_ENT_0800:"):
continue
status = 'start'
#[Users of VCAST_C_ENT_0800: (Total of 2 licenses issued; Total of 1 license in use)
pattern = r"\(Total of (\d+).*Total of (\d+).*\)"
ret = re.findall(pattern, line)
if not ret:
log.warning(f"{lic} parser [{line}] fail")
return info
(total, used) = ret[0]
info.total = int(total)
info.used = int(used)
elif status == 'start':
#zhenhao.he szl3bc12804 /dev/tty (v23) (szmaslic06/27003 337), start Thu 1/2 8:52
pattern = '(\S+) (\S+).*\((\S+)/.*\), start (.*)'
ret = re.findall(pattern, line)
if not ret:
continue
used = UsedInfo()
(used.user,used.host,used.lic,time_string) = ret[0]
#处理跨年的情况
year = datetime.now().year
start_time = datetime.strptime(f"{year} {time_string}", "%Y %a %m/%d %H:%M")
if start_time > datetime.now():
year -= 1
start_time = datetime.strptime(f"{year} {time_string}", "%Y %a %m/%d %H:%M")
used.start_time = start_time
# log.info(f"{lic} => {used}")
info.used_info.append(used)
# Next section
elif line.startswith('Users of '):
break
return info
def draw_time_grpah(license_manager):
info = license_manager.sorted()
max_time = max(info.values())
name_len = max([len(_) for _ in info.keys()])
top = 100
for user, time in info.items():
curr = '' * max(int(top * time / max_time), 1)
name_remain = ' ' * (name_len - len(user))
log.info(f"{user}{name_remain} :{time:5.2f}h {curr}")
if __name__ == '__main__':
log.info("License status query...")
lm = LicenseStatistic()
log.info(lm)
log.info(f"All licenses is: {[str(_) for _ in lm.get_lic_list()]}")
for lic,number,total in lm.available():
log.info(f"{lic}: {number} lic available")
draw_time_grpah(lm)