EDA许可证非常昂贵,及时了解许可证数量和过期时间就显得非常重要,可以避免遗忘许可证过期时间造成业务进度受阻发生。
EDA许可证大都通过 Flexlm 管理,虽然 Flexlm 提供了命令行工具查看许可证数量、版本和过期信息,但以命令行形式显示的数据不够直观,通过图形界面以图形、表格的方式可以提供更多维度、更便捷的数据和更好的用户体验。
如下图所示,可以很方便看到许可证在各时间段的数量,以及详细的数据。
也可以查看指定时间段内过期的许可证。
常规的许可证数据采集和显示架构如下图所示:
许可证数据采集器定时从各许可证服务器获取许可证数据,经过处理后保存到数据库中,用户在客户端WEB界面通过访问WEB服务就可以查询许可证数据,在WEB界面上以图形、表格等形式显示。
许可证数据的采集方法
通过采集和解析以下命令的输出可以获取许可证名称、 厂商服务名称、版本、数量和过期时间。
lmstat -i -c port@lic_server
输出示例:
下面的脚本即通过上面的命令从许可证服务器采集许可证数据,经过处理后保存到文件中。通过 cron 定时调用此脚本就可以及时获取许可证数量和过期数据。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# 通用模块
import os
import sys
import time
import yaml
import signal
import datetime
import subprocess
import logging
import logging.config
import traceback
import pendulum as dtm
from hashlib import sha1
# 数据库和数据处理模块
import pandas as pd
# 多进程处理模块
import multiprocessing
from concurrent.futures import ProcessPoolExecutor
LOG_FILE = '/tmp/lic_expiry.log'
LOGGERCONFIG = {'version': 1.0,
'disable_existing_loggers': False,
'formatters': {'log': {'format': '%(asctime)s:%(levelname)s:%(process)d:%(lineno)d:%(funcName)s: %(message)s'},
'print': {'format': '%(message)s'}},
'handlers': {'rlog': {'class': 'logging.handlers.RotatingFileHandler',
'filename': LOG_FILE,
'mode': 'a',
'maxBytes': 50000000,
'backupCount': 2,
'formatter': 'log'},
'console': {'class': 'logging.StreamHandler',
'stream': 'ext://sys.stdout',
'formatter': 'log'}},
'root': {'handlers': ['rlog'], 'level': 'INFO'}}
def readConf(cfname):
cfpath = os.path.join(os.path.dirname(__file__), f"{cfname}")
if not os.path.exists(cfpath):
logging.warning(f"Missing configuration file : {cfpath}")
sys.exit(1)
with open(cfpath, 'r') as fd:
return yaml.load(fd, Loader=yaml.FullLoader)
def timeit(func):
def timing(*args, **kwargs):
t = time.perf_counter()
result = func(*args, **kwargs)
delta = time.perf_counter() - t
logging.info(f"{func.__name__} {delta: >,.4f} s")
return result
return timing
def executer(cmd, timeout=600):
rc, ret, stdout, stderr = None, None, None, None
try:
if isinstance(cmd, list):
cmd = ' '.join(cmd)
ret = subprocess.run(cmd, shell=True, capture_output=True, timeout=timeout, preexec_fn=os.setsid)
except Exception as e:
logging.error('CMD: [{}] failed. Error: {}, Stack: {}'.format(cmd, str(e), traceback.format_exc()))
stdout = str(e.stdout.strip(), encoding='utf-8', errors='ignore') if 'stdout' in dir(e) else ''
stderr = str(e)
else:
if ret:
rc = ret.returncode
stdout = str(ret.stdout.strip(), encoding='utf-8', errors='ignore') if 'stdout' in dir(ret) else ''
stderr = str(ret.stderr.strip(), encoding='utf-8', errors='ignore') if 'stderr' in dir(ret) else ''
return [rc, stdout, stderr]
class licFeatureParser():
def __init__(self, licServer):
self._licServer = licServer.strip()
self._port, self._server = licServer.strip().split('@')
self._port = int(self._port)
self._server = self._server.strip()
self._licData = ''
self._termFlag = False
self._licFeature = []
self._lic_feature = pd.DataFrame()
def _termHandler(self, signum, frame):
self._termFlag = True
logging.info("Loader %d for %s received signal %d, quit"%(os.getpid(), self._licServer, signum))
sys.exit(0)
def run(self):
logging.info(f"Get license data from {self._licServer}")
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
signal.signal(sig, self._termHandler)
st = time.time()
self._getData()
self._parseData()
self._transData()
et = time.time()
delta = et - st
logging.info(f"Get license data from {self._licServer} takes {round(delta, 2)} seconds.")
return {'lic_feature_expiry':self._lic_feature}
@timeit
def _getData(self):
'''
通过运行lmstat获得许可证信息并更新数据库
:param licServer: 许可证服务器,格式 port@server
:param epcon: ES Pandas连接
:return : 成功/失败
'''
licServer = self._licServer
logging.info(f"Get license data from {licServer}")
try:
# 通过lmstat采集许可证使用数据
cmd = f"lmutil lmstat -i -c {licServer} |awk 'NF >= 5 {{print $0}}'|egrep -v 'lmstat -a|___|Expires|Copyright|NOTE|but only'"
(rc, sout, serr) = executer(cmd, timeout=10)
if rc:
self._loaderFailed = True
logging.error("Get license data from {} failed. CMD[{}] Return code[{}], output[{}], error[{}]".format(licServer, cmd, rc, sout, serr))
else:
self._licData = sout.splitlines()
logging.info(f"Total {len(self._licData)} lines of data.")
except Exception as e:
logging.error("Get license data from {} failed. CMD:{}, Error:{}, stack:{}".format(licServer, cmd, str(e), traceback.format_exc()))
@timeit
def _parseData(self):
'''
解析许可证数据
:params self._licData: lmstat -i -c 输出
:return:
'''
permanent = dtm.datetime(2099,1,1, tz='Asia/Shanghai')
for line in self._licData:
line = line.strip()
if len(line) == 0:
continue
(feature, version, number, vendor, expiry) = line.split()[:5]
today = dtm.now()
eday = self._parseExpiry(expiry)
if eday != permanent and eday < today:
continue
id = hash(f"{self._licServer}{vendor}{feature}{version}{number}{expiry}")
self._licFeature += [{'_id':id, 'server':self._licServer, 'feature':feature, 'version': version, 'number': number, 'vendor':vendor, 'expiry':eday.int_timestamp*1000}]
def _transData(self):
self._lic_feature = pd.DataFrame(self._licFeature) if len(self._licFeature) else pd.DataFrame()
self._setIndex()
def _setIndex(self):
if self._lic_feature is not None and not self._lic_feature.empty:
self._lic_feature.set_index('_id', inplace=True)
def _resetIndex(self):
if self._lic_feature is not None and not self._lic_feature.empty:
self._lic_feature.reset_index(inplace=True)
def _parseExpiry(self, dstr):
'''
Parse date time format like '06-jun-2023'
:param dstr: 日期时间字符串
:return: datetime
'''
permanent = dtm.datetime(2099,1,1, tz='Asia/Shanghai')
try:
ed = permanent if dstr.startswith('permanent') else dtm.from_timestamp(datetime.datetime.strptime(dstr, '%d-%b-%Y').timestamp(), tz='Asia/Shanghai')
except Exception as e:
logging.error(f"Failed to parse {dstr}")
ed = dtm.from_timestamp(0, tz='Asia/Shanghai')
return ed
def dataLoader(server):
loader = licFeatureParser(server)
return loader.run()
def main():
logging.config.dictConfig(LOGGERCONFIG)
logger = logging.getLogger(os.path.basename(__file__))
termFlag = False
# 主进程信号处理器
def term_handler(signum, frame):
termFlag = True
logging.info("Poller %d received signal %d, quit"%(os.getpid(), signum))
# 转发信号给所有子进程
cplist = multiprocessing.active_children()
for child in cplist:
pid = child.pid
logging.info("Send signal %d to child %d"%(signum, pid))
os.kill(pid, signum)
# 启动信号处理
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
signal.signal(sig, term_handler)
licServer = readConf("lic_server.yaml")
workers = len(licServer)
st = time.time()
logging.info("Get license expiry data.")
licData = pd.DataFrame()
with ProcessPoolExecutor(max_workers=workers) as worker:
for server, data in zip(licServer, worker.map(dataLoader, licServer)):
logging.info(f"License server {server} has {len(data['lic_feature_expiry'])} license expiry")
if data is not None and not data['lic_feature_expiry'].empty:
licData = pd.concat([licData, data['lic_feature_expiry']])
licData.drop_duplicates(inplace=True)
logging.info(f"Total {len(licData)} license expiry")
logging.info("Save license expiry data.")
fname = os.path.join(os.path.dirname(__file__), "lic_feature_expiry.csv")
licData.to_csv(fname)
del licData
et = time.time()
logging.info(f"Takes {et - st} s to load license data.")
if __name__ == "__main__":
main()
通过 lic_server.yaml 文件提供许可证服务器列表,格式如下
- port1@lic_server1
- port2@lic_server2
- port3@lic_server3
...
脚本运行完成后会在当前目录下生成 文件 lic_feature_expiry.csv
格式说明
字段名称 | 说明 |
_id | 数据行的标识,通过计算一行数据的散列值获取 |
server | 许可证服务器,格式为 port@lic_server |
feature | 许可证名称 |
version | 许可证版本 |
number | 许可证数量 |
vendor | 厂商服务标识 |
expiry | 许可证过期时间,以毫秒时间戳表示 |
数据采集到后,即可通过大数据工具比如 pandas 处理并绘制图形和表格,或者通过编写数据处理API再通过 echarts 等工具在WEB上显示。