#!/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)