diff --git a/license.py b/license.py new file mode 100644 index 0000000..d96a70e --- /dev/null +++ b/license.py @@ -0,0 +1,267 @@ +#!/usr/bin/python3 +import json +import queue +import re +import sys +import socket +import asyncio +import threading +import time +from typing import Dict, List +from datetime import datetime +from utils import * +import logger +log = logger.get_logger() + +def _get_ip_address(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}") + 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): + 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' + } + return str(obj) + def __repr__(self): + return self.__str__() + +class LicenseInfo: + def __init__(self, name='', total=0, used=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 = { + "name":self.name, + "total":self.total, + "used":self.used, + "used_info":self.used_info + } + return str(obj) + def __repr__(self): + return self.__str__() + + +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() + + 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 _in_wait_queue(self, user): + return any(user == _.user for _ in list(self.wait_queue)) + + def request(self, user:str, host:str): + #如果已经是在线的状态就不允许申请 + 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 = { + "user":user, + "host":host, + "start_time":time.now() + } + self.wait_queue.put(data) + return False + + def cancel(self): + pass + + def release(self): + pass + + def get_wait_queue(self): + return [_ for _ in self.wait_queue.queue] + +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.get_event_loop() + infos = loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) + for info in infos: + if info is not None: + self.licenses[info.name] = info + if not self.licenses: + log.warning("not vcast lic found") + 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的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__() + +""" + {"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" + 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 None + (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) + 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(): + log.info(f"{lic}: {number} lic available") + draw_time_grpah(lm) \ No newline at end of file diff --git a/server.py b/server.py index d724586..44a441d 100755 --- a/server.py +++ b/server.py @@ -5,39 +5,121 @@ import os from pathlib import Path sample_data = { - "27003@szmis03": { - "name": "27003@szmis03", - "total": 2, - "used": 2, - "used_info": [ - { - "user": "ekko.bao", - "host": "szl3bc1808", - "login_time": "58.9h" - }, - { - "user": "zzc", - "host": "szl3bc1808", - "login_time": "58.9h" - } - ] - }, - "27003@szmis05": { - "name": "27003@szmis05", - "total": 2, - "used": 0, - "used_info": [] - }, - "27003@szmis10": { - "name": "27003@szmis10", - "total": 2, - "used": 1, - "used_info": [{ - "user": "zzc33333", - "host": "szl3bc1808", - "login_time": "58.9h" - }] - } + "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" + } + ] + } } class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): diff --git a/src/services/licenseService.js b/src/services/licenseService.js index 3400754..4f2ca85 100755 --- a/src/services/licenseService.js +++ b/src/services/licenseService.js @@ -10,12 +10,12 @@ const mockData = { { "user": "ekko.bao", "host": "szl3bc1808", - "login_time": "3.4h" + "login": "3.4h" }, { "user": "zzc", "host": "szl3bc1808", - "login_time": "6.9h" + "login": "6.9h" } ] }, @@ -32,7 +32,7 @@ const mockData = { "used_info": [{ "user": "zzc33333", "host": "szl3bc1808", - "login_time": "58.9h" + "login": "58.9h" }] } }; diff --git a/src/utils/dataTransformer.js b/src/utils/dataTransformer.js index d1b3acd..c581300 100755 --- a/src/utils/dataTransformer.js +++ b/src/utils/dataTransformer.js @@ -4,7 +4,7 @@ import { convertTimeToMinutes } from './timeConverter.js'; function getMaxDuration(license) { if (!license.used_info.length) return 0; return Math.max(...license.used_info.map(info => - convertTimeToMinutes(info.login_time) + convertTimeToMinutes(info.login) )); } @@ -19,7 +19,7 @@ export function transformLicenseData(data) { usageDetails: license.used_info.map(info => ({ userName: info.user, hostName: info.host, - duration: convertTimeToMinutes(info.login_time) + duration: convertTimeToMinutes(info.login) })) })) .sort((a, b) => b.maxDuration - a.maxDuration); // 按最大使用时长降序排序