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上显示。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐