# 监视器ubuntu20.04 网络配置 bsymv
# last modify by cls on 20210830
# 查找proc剔除无线网卡
import sys
import time
import re
import os
import json
import math
import ctypes
from ctypes import *
import time
from threading import Timer
import fcntl
import subprocess
import select

print("version 2.0.0 for ubuntu2004-moju on %s" % time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))

workpath = os.path.dirname(os.path.realpath(__file__))    # 从当前文件路径中获取目录

netplan_yaml = "/etc/netplan/default_netplan.yaml"  # netplan配置文件 yaml
netplan_config_path = "/etc/netplan/netplan_prejson.txt"
# c/c++动态库
#curpath = os.getcwd()
clib_clsnt_path = workpath + "/" + "libcls_netclib.so"  # cls network tool

#clib_clsnt_path = "/root/cls_netclib/" + "libcls_netclib.so"  # cls network tool

# 解析json 配置文件内容netplan_yaml = "/etc/netplan/default_netplan.yaml"  # netplan配置文件 yaml

# netplan_yaml = "/root/bin-AOI/cnt_net/netplan_preyaml.yaml"
# netplan_config_path = "/root/bin-AOI/cnt_net/netplan_prejson.txt"  # 配置文件json（自定义）

# function---------------------------------------------------------------------------
# 删除转义反斜杠
def removequote(str: str):
    str = str.replace("\\\\", "\\")
    # str = str.replace("\\\"", "\"")
    return str


# 掩码ip转掩码长度
def mask2masklen(maksip: str):
    retback = 24  # 如果解析错误 默认返回值24
    sppart = maksip.split(".")
    if len(sppart) != 4:
        return retback
    retback = 0
    for item in sppart:
        i = int(item)
        log2 = math.log(i + 1, 2)  # 求log以2为底i+1的对数
        num = int(log2)
        retback += num
    return retback


def isWifi(name: str):
    path = "/sys/class/net/%s/wireless" % name
    if os.path.exists(path):
        return True
    return False


# 执行函数 默认20秒超时
def make_nonblock(fd):
    flags = fcntl.fcntl(fd, fcntl.F_GETFL)
    fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)


class CCommand:
    TIMEOUT_MSG = ""

    def __init__(self, cmd):
        self.runtime = 0
        self.proc = subprocess.Popen(
            cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
        fd_stdout = self.proc.stdout
        flags = fcntl.fcntl(fd_stdout, fcntl.F_GETFL)
        fcntl.fcntl(fd_stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK)
        fd_stderr = self.proc.stderr
        flags = fcntl.fcntl(fd_stderr, fcntl.F_GETFL)
        fcntl.fcntl(fd_stderr, fcntl.F_SETFL, flags | os.O_NONBLOCK)

    def command_run(self, timeout=20):
        self.runtime = time.time()
        timer = None
        ret = {}
        out_ret = ""
        err_ret = ""
        if timeout:
            timer = Timer(timeout, self._kill_proc, [self.proc, ])
            timer.setDaemon(True)
            timer.start()
        while True:
            out_ret += self._read(self.proc.stdout)
            err_ret += self._read(self.proc.stderr)
            if self.proc.poll() is not None:  # 不在运行状态
                break
        out_ret += self._read(self.proc.stdout)
        err_ret += self._read(self.proc.stderr)
        self.runtime = time.time() - self.runtime  # time
        ret["OUT"] = out_ret
        ret["ERR"] = self.TIMEOUT_MSG + err_ret
        ret["STATUS"] = self.proc.returncode
        ret["RUNTIME"] = "%.3f s" % self.runtime
        if timer:
            timer.cancel()
            timer.join()
        self.proc.stdout.close()
        return ret

    def _read(self, fd):
        out = ""
        if fd is None or fd.closed:
            return out
        rd, _, _ = select.select([fd], [], [], 0)
        if rd:
            out = fd.readline()
            out = out.decode(encoding='utf-8')
        return out

    def _kill_proc(self, proc):
        if proc:
            try:
                proc.kill()
                self.TIMEOUT_MSG = "timeout:"
            except OSError:
                pass
            finally:
                proc.stdout.close()


# 此程序函数---------------------------------------------------------------------------
# 检查并构建 新的node内容, listnode接修改后的结构组
# 判断重复的网段
# 检查使用唯一的网关
def reBuildNodes(listnode: dict):
    worknodes = list(listnode.keys())

    # 检查是否有注册的网关信息, 有写网关的网卡优先使用ip, 不判断修改
    register_route = chkbuildNodes_findroute(worknodes, listnode)
    if register_route and register_route in worknodes:
        worknodes.remove(register_route)

    # 全部按静态处理剩余项
    chkbuildNodes_repetition(worknodes, listnode)


def chkbuildNodes_findroute(worknodes: list, dictchk: dict) -> str:
    register_route = ""  # 需要使用的网关信息 ->mac
    # 先找dhcp设置的项
    for inode in dictchk.values():
        if inode.mac not in worknodes:
            continue
        if inode.isdhcp:
            inode.status = "normal"
            inode.describe = "使用dhcp获取动态ip"
            inode.address = ""
            inode.netmask = ""
            inode.gateway = ""
            inode.dns = ""
            register_route = inode.mac
            break
    if register_route:
        return register_route
    for inode in dictchk.values():
        if inode.mac not in worknodes:
            continue
        if inode.gateway:  # 如果有设置gateway, 判断ip是否是同一网段
            search_ip = re.findall("^((\d+.\d+.\d+).\d+)", inode.address, re.M)
            search_route = re.findall("^((\d+.\d+.\d+).\d+)", inode.gateway, re.M)
            # [('192.168.5.5', '192.168.5')]
            if not search_ip or not search_route:
                continue
            part1_ip = search_ip[0][1]
            part1_route = search_route[0][1]
            if part1_ip == part1_route:
                inode.status = "normal"
                inode.describe = "优先使用的网关"
                inode.isdhcp = False
                if not inode.dns:
                    inode.dns = inode.gateway
                register_route = inode.mac
                break
    return register_route


def chkbuildNodes_repetition(worknodes: list, dictchk: dict):
    listregister = []

    # 跳过静态ip设置的项
    routeitem = list(set(worknodes) ^ dictchk.keys())
    if routeitem and routeitem[0] in dictchk.keys():
        useipbyroute = dictchk[routeitem[0]].address
        search_ip = re.findall("^((\d+.\d+.)(\d+).\d+)", useipbyroute, re.M)
        if search_ip:
            part1 = search_ip[0][1]
            ipart2 = int(search_ip[0][2])
            opart = part1 + "%d" % ipart2
            listregister.append(opart)

    for inode in dictchk.values():
        if inode.mac not in worknodes:
            continue
        # 判断ip是否重复
        ip = inode.address
        if not ip:
            ip = "192.168.3.5"
        search_ip = re.findall("^((\d+.\d+.)(\d+).\d+)", ip, re.M)
        if not search_ip:
            continue
        # [('192.168.1.77',  '192.168.', '1')]
        aip = search_ip[0][0]
        part1 = search_ip[0][1]
        ipart2 = int(search_ip[0][2])
        opart = part1 + "%d" % ipart2
        startipart2 = 3
        while True:
            if startipart2 > 253 or startipart2 < 3:
                break
            if opart not in listregister:  # 没有使用过的网段
                inode.address = aip
                listregister.append(opart)
                inode.describe = "设置成功"
                break
            else:
                startipart2 = startipart2 % 255 + 1
                opart = part1 + "%d" % startipart2
                inode.status = "warning"
                inode.describe = "存在相同的网段,已经更新至 %s" % opart
                aip = opart + ".5"
        inode.netmask = "255.255.255.0"
        inode.gateway = ""
        inode.dns = ""
        inode.isdhcp = False


# 创建netplan规则文件
def buildNetPlanFile(listnode):
    str = ""
    use_t = "    "  # 4空格
    str += "# This is the network config written by 'subiquity'" + "\n"
    str += "network:" + "\n"
    str += use_t + "ethernets:" + "\n"
    for node in listnode.values():
        str += node.getYamlStr()
    str += use_t + "version: 2" + "\n"
    str += use_t + "renderer: NetworkManager" + "\n"

    os.system("rm -f /etc/netplan/*.yaml")
    with open(netplan_yaml, "w+") as yaml_f:
        yaml_f.write(str)


# netplan_yaml = "/root/bin-AOI/cnt_net/netplan_preyaml.yaml"
def LoadConfig2Nodes(listnode):
    loaddict = {}
    with open(netplan_config_path, "r") as json_f:
        loaddict = json.load(json_f)
    jsonnodes = loaddict["netcard_info"]

    try:
        for inode in jsonnodes:
            imac = inode["mac"]
            if imac not in listnode.keys():
                continue
            node = listnode[imac]
            if inode["address"] and inode["address"] != "0.0.0.0":
                node.address = inode["address"]
            if inode["netmask"] and inode["netmask"] != "0.0.0.0":
                node.netmask = inode["netmask"]
            if inode["gateway"] and inode["gateway"] != "0.0.0.0":
                node.gateway = inode["gateway"]
            if inode["dns"] and inode["dns"] != "0.0.0.0":
                node.dns = inode["dns"]
            node.isdhcp = inode["isdhcp"]
    except KeyError as Argument:
        print("KeyError", Argument)


# class---------------------------------------------------------------------------
class ND_Node:
    def __init__(self):
        self.mac = ""
        self.name = ""
        self.address = ""
        self.netmask = "255.255.255.0"
        self.gateway = ""
        self.dns = ""
        self.isdhcp = False
        self.describe = ""
        self.describe_netplan = ""
        self.status = "normal"  # normal, warning, error

    def getYamlStr(self):
        str = ""
        use_t = "    "

        str += (use_t * 2) + self.name + ":" + "\n"
        if self.isdhcp:
            str += (use_t * 3) + "dhcp4: " + "True" + "\n"
            return str
        else:
            str += (use_t * 3) + "dhcp4: " + "false" + "\n"

        str += (use_t * 3) + "addresses: " + "[" + self.address + "/" + "%d" % mask2masklen(
            self.netmask) + "]" + "\n"
        str += (use_t * 3) + "mtu: " + "%d" % 9000 + "\n"
        if self.gateway and self.gateway != "0.0.0.0":
            str += (use_t * 3) + "gateway4: " + self.gateway + "\n"
            str += (use_t * 3) + "nameservers: " + "\n"
            str += (use_t * 4) + "addresses: " + "[" + self.dns + "]" + "\n"
        return str

    def getReport(self):
        return self.__dict__

    def rechkafterplan(self, libc):
        class CSTU_INFO(Structure):
            _fields_ = [("ip", c_char * 20),
                        ("mask", c_char * 20)]

        obs_info = CSTU_INFO()  # object struct info
        ptr_stu = byref(obs_info)  # to -> ptr
        # func
        cfun_getinfo = libc.NC_getEnInfo
        cfun_getinfo.restype = c_bool
        # libnet.argtypes = [c_char_p, CSTU_INFO]
        name = create_string_buffer(self.name.encode("utf-8"))
        rback = cfun_getinfo(name, ptr_stu)

        curip = str(obs_info.ip, 'utf-8')
        curmask = str(obs_info.mask, 'utf-8')

        # print(rback, curip, curmask)

        if rback:
            self.describe_netplan = ""

            if self.gateway or self.isdhcp:
                route = ""
                dns = ""

                # route
                dcmd = CCommand("ip route show")
                iprouteshow = dcmd.command_run()
                back = iprouteshow["OUT"]
                reachroute = re.findall("^default via (\d+.\d+.\d+.\d+)\sdev\s(\S+)\sproto.*", back)
                if reachroute and reachroute[0][1] == self.name:
                    route = reachroute[0][0]
                # dns
                etc_resolv = ""
                with open("/etc/resolv.conf", "r") as f_resolv:
                    etc_resolv = f_resolv.read()
                reachdns = re.findall("nameserver (\d+.\d+.\d+.\d+)", etc_resolv)
                if reachdns:
                    dns = reachdns[0]

                if not route:
                    self.describe_netplan = "网关未配置成功"
                    self.status = "warning"
                if not dns:
                    self.describe_netplan = "DNS未配置成功"
                    self.status = "warning"
                self.address = curip
                self.netmask = curmask
                self.gateway = route
                self.dns = dns
                self.describe_netplan = "获取成功,正常使用"

                # 检查是否有和其他网卡的网段冲突
                for otheritem in gMName4Node.keys():
                    if otheritem == self.mac:
                        continue
                    objitem = gMName4Node[otheritem]
                    reachpart = re.findall("(\d+.\d+.\d+).\d+", objitem.address)
                    reachpart_in = re.findall("(\d+.\d+.\d+).\d+", self.address)
                    if reachpart and reachpart_in:
                        if reachpart_in and reachpart[0] == reachpart_in[0]:
                            self.describe_netplan = "与%s网卡使用的网段冲突了" % objitem.name
                            self.status = "error"
            else:
                # 判断获取的和预设的是否一样
                if curip != self.address or curmask != self.netmask:
                    self.describe_netplan = "配置结果与预设值有偏差" + curip + "/" + curmask
                    self.status = "warning"
                else:
                    self.describe_netplan = "正常使用"

            return True
        else:
            self.describe_netplan = "网卡可能未使用"
            self.status = "warning"

        return False


# main---------------------------------------------------------------------------
if __name__ != "__main__":
    exit(0)

cdll.LoadLibrary(clib_clsnt_path)
libc_clsnt = CDLL(clib_clsnt_path)

os.system("mkdir -p ./cnt_net")
gMName4Node = {}  # key:网卡名 value:对象

# /proc/net/dev : listproc_cardname ['enp4s0', 'enp2s0', 'lo', 'enp3s0', 'enp6s0', 'enp5s0']
# 获取mac和网卡名对应的字典 : dict_mac2name {'b0:41:6f:04:5a:5e': 'eno1', 'b0:41:6f:04:4b:d6': 'enp2s0'}
dict_mac2name = {}
f = open("/proc/net/dev", "r")
str_cmd = f.read()
f.close()
listproc_cardname = re.findall("\s+(\S+):", str_cmd)
listwifi = []
for iproc in listproc_cardname:
    if iproc == "lo":
        continue
    if isWifi(iproc):
        listwifi.append(iproc)
        continue
    macFile = "/sys/class/net/%s/address" % iproc  # iproc网卡名称
    with open(macFile, "r") as fitem:
        mac = fitem.read()
        mac = mac.strip("\n")  # strip()可以去除头尾指定字符
        dict_mac2name[mac] = iproc
listproc_cardname = list(set(listproc_cardname) ^ set(listwifi))
print(listproc_cardname)

# 初始化列表node
for imac in dict_mac2name:
    inode = ND_Node()
    inode.mac = imac
    inode.name = dict_mac2name[imac]
    mask2masklen(inode.netmask)

    gMName4Node[imac] = inode

# 读取配置文件
isexist_ncfg = os.path.exists(netplan_config_path)
if isexist_ncfg:
    LoadConfig2Nodes(gMName4Node)  # 重新构建配置文件, 处理网段重复问题
reBuildNodes(listnode=gMName4Node)

# 解析成netplan文件
buildNetPlanFile(gMName4Node)

# 执行网络计划
dcmd = CCommand("netplan apply")
netplan_back = dcmd.command_run()
print("netplan apply time:" + netplan_back["RUNTIME"])

# 等待网络配置plan 获取报表
waitreporttime = time.time()
afterplan_waitchk = gMName4Node.keys()
roundtimes = 0
while roundtimes < 10 and afterplan_waitchk:
    savelist = []
    for item in afterplan_waitchk:
        itemobj = gMName4Node[item]
        if not itemobj.isdhcp and not itemobj.gateway and roundtimes > 4:
            continue
        back = itemobj.rechkafterplan(libc_clsnt)  # 检查计划执行结果
        if back:
            continue
        savelist.append(item)
    afterplan_waitchk = savelist
    roundtimes += 1
    if afterplan_waitchk:
        time.sleep(1)
waitreporttime = time.time() - waitreporttime
print("wait report time:%.3f s" % waitreporttime)

# 保存新的配置文件
mapReport = {}
report = []
for item in gMName4Node.values():
    report.append(item.getReport())
mapReport["netcard_info"] = report
jsonstr = json.dumps(mapReport, indent=4, separators=(',', ':'), ensure_ascii=False)
jsonstr = removequote(jsonstr)
with open(netplan_config_path, "w+") as f_report:
    f_report.write(jsonstr)

print("finish")
