摘要

首先说明,以下几类读者请自行对号入座:

  • 对CMDB很了解但对于Python还没有上手的读者,强烈建议阅读前面几篇;
  • 对Python了解较少只能写出简单脚本的读者,强烈建议阅读此篇;
  • 已经可以熟练写出Python脚本,但对CMDB不是很了解的读者,建议阅读此篇;
  • 即了解Python,又了解CMDB的读者,可以出门左转,看下一篇。

前面几节我们完成了CMDBv1.0版本最难的部分的讲解,这节内容我们就带领大家一次将删除和查询功能分析完成。话不多说上干货。

上干货

代码优化

之前我们的新增和更新信息的功能中都有对attrs做校验和解析,那么我们是不是可以将其抽象成一个新的函数,如下:

def check_parse(attrs):
    if attrs is None:  # 判断attrs的合法性
        print("attributes is None")
        return
    try:
        attrs = json.loads(attrs)
        return attrs
    except Exception:
        print("attributes is not valid json string")
        return
    
def add(path, attrs=None):
    attrs = check_parse(attrs)
    if not attrs:
        return
    ...

def update(path, attrs=None):
    attrs = check_parse(attrs)
    if not attrs:
        return
    ...

恭喜我们的代码又成功减少几行

删除资产信息

这一节我们就省略五步法的一些步骤,只对最关键的功能进行一下思考

  • 在任何场景中一旦涉及到删除功能,就需要慎之又慎,绝不能多删误删,不然可能就要背锅走人了,那么我们在删除资产信息时需要注意什么呢,其实有时候为了保险期间我们会尽量使用更新去代替删除,但有一些多余的属性信息又不得不删除,那么如果我们删除的路径上是一个字符串或者数字还比较简单,如果是一个字典,或者是一个数组,就需要格外注意了。
  • 另外就是对于我们的参数,我们是否需要同时传入pathattrs

源代码如下:

def delete(path, attrs=None):
    attrs = check_parse(attrs)
    path_seg = path.split("/")[1:]
    data = read_file()
    target_path = data
    for idx, seg in enumerate(path_seg):
        if seg not in target_path:
            print("delete target path not in data.")
            return
        if idx == len(path_seg)-1:
            if not attrs:
                target_path.pop(seg)
                break
            if isinstance(attrs, list):
                for attr in attrs:
                    if attr not in target_path[seg]:
                        print("attr %s not in target_path" % attr)
                        continue
                isinstance(target_path[seg], dict):
                    target_path[seg].pop(attr)
                if isinstance(target_path[seg], list)
                	target_path[seg].remove(attr)
                break
        target_path = target_path[seg]
    write_file(data)
    print(json.dumps(data, indent=2))
  • 这里首先仍然是对传入的属性值做解析,我们为什么不像addupdate一样复用check_parse()方法,当解析到的attrsNone时就退出函数呢,这里是因为我们的删除功能,可以不传attrs参数,有时候我们的目的就是直接删除数据源中的这个路径下的所有属性,那么就只需要传入path即可。
  • 在查找指定路径的时候我们同样也做了优化,如下:
for idx, seg in enumerate(path_seg):
    if seg not in target_path:
        print("delete target path not in data.")
        return
    ...

可以和之前定位路径的代码做一下对比:

for idx, seg in enumerate(path_seg):
    if idx == len(path_seg)-1:
        if seg not in target_path:
        	print("delete path is not exists in datan")
        	return
    ...

我们之前在定位路径时,对path做了分割,只有在segpath_seg的最后一个元素时才去判断是否这个segtarget_path上,这样就会导致程序运行很多无用的循环逻辑。

优化之后我们在每次循环的一开始就对seg做了判断,因为如果被分割开的path_seg中任何一段seg不在数据源路径中时,那么整段path就必然不可能在数据源中定位到,所以我们一旦检测到当前的seg不在target_path时就可以直接退出函数

  • 删除功能中的核心代码块如下:
if idx == len(path_seg)-1:  # 循环中定位到指定路径
    if not attrs:
        target_path.pop(seg)
    if isinstance(attrs, list):
        for attr in attrs:
            if attr not in target_path[seg]:
                print("attr %s not in target_path" % attr)
                continue
            if isinstance(target_path[seg], dict):
                target_path[seg].pop(attr)
            if isinstance(target_path[seg], list):
                target_path[seg].remove(attr)
    break

删除属性主要分为三个部分:

  1. 当我们没有传入要删除的attrs时,我们默认删除该路径下的所有内容,这里用到的操作是字典的删除功能dict.pop(),这个方法要求传入一个字典的键值,键值如果不存在会抛出异常,但由于我们在每次循环时都判断了seg是否在target_path中,所以程序运行到这里的话,这个路径就必然是存在的,那么我们通过target_path.pop(seg)就可以将该路径下面的属性全部删除
if not attrs:
    target_path.pop(seg)

Tips: 安全性

其实我们考虑到数据的安全性,应该在删除指定路径的全部属性时做一个判断,因为如果是忘记了输入attrs而造成了误删,那可能直接就一个P1了,所以我们可以这里将attrs传入一个all或者类似的标志,来表示确定删除指定路径下的全部属性。

  1. 当我们的指定路径下是一个字典并且传入的属性attrs是一个数组的时候,我们就去遍历attrs,将其元素一次从target_path下删除,这里有注意点就是我们在上面已经提到,dict.pop()必须传入字典中存在的键,所以我们在循环attrs时,需要先判断这个要删除的元素是否存在,如果不存在则使用continue跳过
if isinstance(attrs, list):
    for attr in attrs:
        if attr not in target_path[seg]:
            print("attr %s not in target_path" % attr)
            continue
        if isinstance(target_path[seg], dict):
            target_path[seg].pop(attr)
  1. 当我们的指定路径下是一个数组,并且传入的属性attrs也是一个数组的时候,我们仍然通过遍历attrs的方式,将attrs中的元素依次从指定路径的数组下面移除,从数组中删除元素使用到了方法list.remove(),这个方法同样要求传入数组中已存在的元素,如果传入的元素不存在则会抛出异常。
if isinstance(attrs, list):
    for attr in attrs:
        if attr not in target_path[seg]:
            print("attr %s not in target_path" % attr)
            continue
        if isinstance(target_path[seg], dict):
            target_path[seg].remove(attr)
查询资产信息

终于到了增删改查的最后一个方法,其实查找是这四个方法中最为简单的,只需要定位到指定路径然后输出就好了,代码如下:

def get(path):
    path_seg = path.split("/")[1:]
    data = read_file()
    target_path = data
    for idx, seg in enumerate(path_seg):
        if seg not in target_path:
            print("get path is not exists in data")
            return
        if idx == len(path_seg)-1:
            break
        target_path = target_path[seg]
    print(json.dumps(target_path, indent=2))

不知道读者朋友们有没有觉得这段代码很眼熟,有没有触动你想要重构之前代码的想法。

完整重构:
import json
from os import read
import sys
from typing import Iterable


def read_file():
    with open("data.json", "r+") as f:
        data = json.load(f)
    return data


def write_file(data):
    with open("data.json", "w+") as f:
        json.dump(data, f, indent=2)


def check_parse(attrs):
    if attrs is None:  # 判断attrs的合法性
        print("attributes is None")
        return
    try:
        attrs = json.loads(attrs)
        return attrs
    except Exception:
        print("attributes is not valid json string")
        return


def locate_path(data, path):
    target_path = data
    path_seg = path.split("/")[1:]
    for seg in path_seg[:-1]:
        if seg not in target_path:
            print("update path is not exists in data, please use add function")
            return
        target_path = target_path[seg]
    return target_path, path_seg[-1]

def init(region):
    with open("data.json", "r+") as f:
        data = json.load(f)
    if region in data:
        print("region %s already exists" % region)
        return
    data[region] = {"idc": region, "switch": {}, "router": {}}
    with open("data.json", "w+") as f:
        json.dump(data, f, indent=2)
    print(json.dumps(data, indent=2))


def add(path, attrs=None):
    attrs = check_parse(attrs)
    if not attrs:
        return
    with open("data.json", "r+") as f:
        data = json.load(f)
    target_path, last_seg = locate_path(data, path)
    if last_seg in target_path:
        print("%s already exists in %s, please use update operation" % (last_seg, path))
        return
    target_path[last_seg] = attrs
    with open("data.json", "w+") as f:
        data = json.dump(data, f, indent=2)
    print(json.dumps(data, indent=2))


def update(path, attrs):
    attrs = check_parse(attrs)
    if not attrs:
        return
    data = read_file()
    target_path, last_seg = locate_path(data, path)
    if type(attrs) != type(target_path[last_seg]):
        print("update attributes and target_path attributes are different type.")
        return
    if isinstance(attrs, dict):
        target_path[last_seg].update(attrs)
    elif isinstance(attrs, list):
        target_path[last_seg].extend(attrs)
        target_path[last_seg] = list(set(target_path[last_seg]))
    else:
        target_path[last_seg] = attrs
    write_file(data)
    print(json.dumps(data, indent=2))


def delete(path, attrs=None):
    attrs = check_parse(attrs)
    data = read_file()
    target_path, last_seg = locate_path(data, path)
    if not attrs:
        target_path.pop(last_seg)
    if isinstance(attrs, list):
        for attr in attrs:
            if attr not in target_path[last_seg]:
                print("attr %s not in target_path" % attr)
                continue
            if isinstance(target_path[last_seg], dict):
                target_path[last_seg].pop(attr)
            if isinstance(target_path[last_seg], list):
                target_path[last_seg].remove(attr)
    write_file(data)
    print(json.dumps(data, indent=2))


def get(path):
    data = read_file()
    target_path, last_seg = locate_path(data, path)
    print(json.dumps(target_path[last_seg], indent=2))


if __name__ == "__main__":
    operations = ["get", "update", "delete"]
    args = sys.argv
    if len(args) < 3:
        print("please input operation and args")
    else:
        if args[1] == "init":
            init(args[2])
        elif args[1] == "add":
            add(*args[2:])
        elif args[1] == "get":
            get(args[2])
        elif args[1] == "update":
            update(*args[2:])
        elif args[1] == "delete":
            delete(*args[2:])
        else:
            print("operation must be one of get,update,delete")

经过我们一起不懈的努力,终于一行一行的读完了CMDBv1.0.py的源代码,理解了对资产信息增删改查的详细逻辑,并且在阅读源码的过程中逐步培养起良好的编程规范和编程思维,这对于大家以会起到至关重要的作用。那么我们到此还没有结束,下一节我们会将CMDBv1.0利用面向对象的思想再次重构为CMDBv1.5,到时候将会是从函数式编程到面向对象编程的一个大的飞跃,敬请期待。