摘要
上一章节中讲解了巡检的设计方案,并实现了SSH执行器的连接功能,不过更为重要的还是希望培养大家一些基础的编程思想和良好的习惯;那么这一章节会着重讲解获取命令和设备的功能,并逐步完善SSH执行器。
一方面由于部分朋友第一次接触较为复杂的模块设计,其次这几个章节中还包含一些Python中较高级的用法,为了让大家便于理解,就先以文件为存储介质,实现命令筛选和设备筛选,后续的话会扩展到与Flask结合,并使用ORM来操作MySQL实现这些功能。
命令筛选
由于是巡检的场景,所以必然是有不同的巡检项的,这些巡检项不需要使用实际的命令行表示,而是统一使用通俗易懂的文字代替,比如:fans_check、power_check等,这样不同厂商实际的命令行就可以对外屏蔽了。
获取命令可以分为两种实现方式,分别是JSON文件和MySQL存储下的数据读写(本章节以JSON文件为主);虽然是两种实现方式,但理论上它们都应该具备“增删改查”的功能,这就恰好可以应用之前讲到的面向对象中的继承的概念。
首先定义一个ActionHandler
表示存储命令的抽象类,该类具有增删改查数据的方法,另外再分别实现ActionJSONHandler
和ActionDBHandler
继承自ActionHandler
。这样的话后续进行命令筛选就不需要区分JSON还是DB类型,因为它们两个都是ActionHandler类型.
ActionHandler:
定义一个具备读写方法的抽象类如下:
class ActionHandler:
def __init__(self, *args, **kwargs):
pass
def add(self, data):
pass
def delete(self, data):
pass
def update(self, data):
pass
def get(self, condition):
pass
上述代码定义了ActionHandler的初始化及增删改查方法,我们想要的效果是:不管什么存储介质,只要继承了ActionHandler并且实现它所有的实例方法即可。但怎么可以保证所有子类都实现这些方法呢?万一不小心忘了怎么办,或者别人接手代码不知道这个规则又怎么办?可能会导致代码异常,并且还较难察觉,而且基于Python解释型语言的特性,运行时才会抛出异常,这种情况显然不是我们想看到的。
但这个问题有办法可以解决,只需要对上述代码加一点点细节即可。
代码重构
# action.py
import json
import abc
from typing import List, Dict, Optional
class Action:
name = ""
description = ""
vendor = ""
model = ""
cmd = ""
type = ""
parse_type = ""
parse_content = ""
class ActionHandler(abc.ABC):
@abc.abstractmethod
def __init__(self, *args, **kwargs) -> None:
pass
@abc.abstractmethod
def add(self, data: List[Dict]) -> None:
pass
@abc.abstractmethod
def delete(self, data: Dict) -> None:
pass
@abc.abstractmethod
def update(self, data: Dict) -> None:
pass
@abc.abstractmethod
def get(self, condition: Optional[Dict] = None) -> List[Action]:
pass
上面的代码看起来好像跟之前章节中写的Python代码有些不一样,多了很多陌生的东西,下面我们就依次来给大家进行讲解。
抽象类
上述代码将ActionHandler定义为了抽象类,并且初始化及增删改查方法都定义成了抽象方法,这时候如果某个类继承了它,但没有实现任意一个方法,那么就会IDE会有提示,并且还会及时抛出异常;
Python中有关于抽象类的工具包——abc(Abstract Base Classes),继承了abc.ABC的类不可被实例化(实际上也不需要被实例化,因为它只是我们规范命令存储对象的抽象类),同时添加了abc.abstractmethod
装饰器的方法必须被子类实现,如果编写代码时某个子类未实现其中某个方法,IDE则会出现如下飘黄报错:
Class ActionJSONMarket must implement all abstract methods
抽象类的使用在很多第三方库的源码中比较常见,Netmiko中就有用到,netmiko.Channel
就是一个抽象基类,要求子类必须实现初始化和读写通道的方法,netmiko.SSHChannel
和netmiko.TelnetChannel
都继承了Channel
基类。
所以在阅读源码的过程中也可以不断的学习到Python的一些高阶技巧,让我们能够把自己的代码编写的更为健壮和可扩展。
函数和变量注解
由于Python是动态语言类型,变量的命名和实际变量指向的对象保存在内存的不同地方,所以在3.5版本之前,变量只是一个名字,它并没有类型,但变量指向的实际对象是有类型的:
var1 = 1 # 整型
var1 = "1" # 字符串
上述代码var1先后从整数变成了字符串,但并不是改变的var1的类型,而是改变了var1的引用对象。
基于上述原因Python会给人一种上手很简单,代码写起来很快的错觉;但应用在大型项目中的时候,就会阅读和维护起来很抓狂,尤其是多人协作时,由于类型信息丢失,看到一个函数或方法,都不知道如何传递参数,该函数会返回什么结果。所以使得代码并不健壮,也不易维护。
在Python3.5和3.6版本先后加入了函数注解和类型注解,不过注解也只是给IDE和人看的,实际运行中并不会进行强制校验;但可以结合pylint对代码做检查,这样对代码的可靠性起到不小的帮助。
后续由于模块的代码量变多,非常有必要引入注解,但注解也并不复杂,所有的类型都在typing
内置库中可以找到,而且常用的也不多,大家可以慢慢熟悉(我也会在视频中提到)。
ActionJSONHandler
可以利用JSON文件作为存储介质,实现命令的读写,数据格式如下:
// action.json
[
{ "name": "fans_check", "description": "风扇检查", "vendor": "h3c", "model": "", "cmd": "display fans", "type": "show", "parse_type": "regexp", "parse_content": "" },
{ "name": "fans_check", "description": "风扇检查", "vendor": "h3c", "model": "", "cmd": "display fans", "type": "show", "parse_type": "textfsm", "parse_content": "" },
{ "name": "fans_check", "description": "风扇检查", "vendor": "cisco", "model": "nexus", "cmd": "show fans", "type": "show", "parse_type": "regexp", "parse_content": "" },
{ "name": "fans_check", "description": "风扇检查", "vendor": "cisco", "model": "ios", "cmd": "show fans", "type": "show", "parse_type": "regexp", "parse_content": "" },
{ "name": "fans_check", "description": "风扇检查", "vendor": "huawei", "model": "", "cmd": "display fans", "type": "show", "parse_type": "regexp", "parse_content": "" }
]
具体代码如下:
import json
import abc
from typing import List, Dict, Optional
class Action:
name = ""
description = ""
vendor = ""
model = ""
cmd = ""
type = ""
parse_type = ""
parse_content = ""
@classmethod
def to_model(cls, **kwargs):
action = Action()
for k, v in kwargs.items():
if hasattr(action, k):
setattr(action, k, v)
return action
def __str__(self):
return json.dumps(self.__dict__)
class ActionHandler(abc.ABC):
@abc.abstractmethod
def __init__(self, *args, **kwargs) -> None:
pass
@abc.abstractmethod
def add(self, data: List[Dict]) -> None:
pass
@abc.abstractmethod
def delete(self, data: Dict) -> None:
pass
@abc.abstractmethod
def update(self, data: Dict) -> None:
pass
@abc.abstractmethod
def get(self, condition: Optional[Dict] = None) -> List[Action]:
pass
class ActionJSONHandler(ActionHandler):
def __init__(self, location: str) -> None:
"""
:param location: 文件的路径
"""
import os
if not os.path.exists(location):
raise Exception("%s path has no exists" % location)
self.path = location
def add(self, data: List[Dict]) -> None:
"""
:param data: List[Dict] 保存的数据
"""
try:
with open(self.path, "r+", encoding="utf-8") as f:
_data = json.load(f)
_data.extend(data)
with open(self.path, "w+", encoding="utf-8") as f:
json.dump(_data, f, ensure_ascii=False)
except Exception as e:
print("save action failed, error: %s" % str(e))
def delete(self, condition: Dict) -> None:
"""
:param condition: List[str] 删除的命令
"""
try:
with open(self.path, "r+", encoding="utf-8") as f:
_data = json.load(f)
_data: List[Dict]
with open(self.path, "w+", encoding="utf-8") as f:
result = []
for idx, item in enumerate(_data):
flag = True
for k, v in condition.items():
if not v or item[k] != v:
flag = False
if not flag:
result.append(item)
json.dump(result, f, ensure_ascii=False)
except Exception as e:
print("delete action failed, error: %s" % str(e))
def update(self, data: Dict) -> None:
"""
:param data: List[Dict] 更新的数据
"""
pass
def get(self, condition: Optional[Dict] = None) -> List[Action]:
"""
:param condition: Dict[Str, Any] 筛选条件
:return: List[Dict]
"""
result = []
try:
with open(self.path, "r+", encoding="utf-8") as f:
data = json.load(f)
if not condition:
return [Action.to_model(**item) for item in data]
for item in data:
for k, v in condition.items():
if not v:
continue
if item[k] != v:
break
else:
result.append(Action.to_model(**item))
except Exception as e:
print("search action by condition failed, error: %s" % str(e))
return result
上述代码就是通过JSON文件来实现命令的增删改查,可以通过以下方法测试:
# action.py
if __name__ == '__main__':
action_json = ActionJSONHandler("action.json")
# res = action_json.get({"vendor": "cisco", "model": "nexus"}) # get by conditions
# action_json.add([{ "name": "fans_check", "description": "风扇检查", "vendor": "huawei", "model": "", "cmd": "display fans", "type": "show", "parse_type": "regexp", "parse_content": "" }])
# action_json.delete({"cmd": "display fans", "vendor": "h3c"})
res = action_json.get()
print(res[0])
设备筛选
之前的章节中已经将设备的数据保存到了MySQL数据库中,并使用ORM来进行了增删改查的操作;有朋友向我咨询问题的过程中谈到觉得ORM要比SQL更复杂,那么这一章节,我就用原生SQL来实现一下设备筛选,大家也可以自行判断一下究竟是哪个更为简单易用。
DeviceHandler
同样定义一个具备增删改查方法的抽象类
import abc
from typing import List, Dict, Optional
# device.py
class DeviceHandler(abc.ABC):
@abc.abstractmethod
def __init__(self, *args, **kwargs) -> None:
pass
@abc.abstractmethod
def add(self, data: List[Dict]) -> None:
pass
@abc.abstractmethod
def delete(self, data: Dict) -> None:
pass
@abc.abstractmethod
def update(self, data: Dict) -> None:
pass
@abc.abstractmethod
def get(self, condition: Optional[Dict] = None) -> List[Device]:
pass
DeviceDBHandler
原先存在两张设备表,分别是devices和device_detail,现在需要修改一下device_detail,增加两列:
alter table device_detail add column (device_type varchar(32), channel varchar(8));
# device.py
import pymysql
from pymysql.cursors import Cursor, DictCursor
class Device:
ip = ""
hostname = ""
vendor = ""
model = ""
hardware = ""
channel = "ssh"
channel_port = 22
device_type = ""
@classmethod
def to_model(cls, **kwargs):
device = Device()
for k, v in kwargs.items():
if hasattr(device, k):
setattr(device, k, v)
if device.channel == "telnet":
device.channel_port = 23
return device
def __str__(self):
return json.dumps(self.__dict__)
class DeviceDBHandler(DeviceHandler):
def __init__(self, user: str, password: str, host: str, database: str, port: int = 3306) -> None:
self.conn = pymysql.connect(user=user, password=password, host=host, port=port, database=database, cursorclass=DictCursor)
self.conn: pymysql.connections.Connection
def get_conn(self) -> Cursor:
if self.conn is None:
raise Exception("mysql is lost connection")
return self.conn.cursor()
def close_db(self):
self.conn.close()
def add(self, data: List[Dict]) -> None:
cursor = self.get_conn()
device_sql = "insert into devices (sn, ip, hostname, idc, vendor, model, role) values (%s, %s, %s, %s, %s, %s, %s);"
device_detail_sql = "insert into device_detail values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s);"
device_data = []
device_detail_data = []
for item in data:
device_data.append([
item.get("sn", ""), item.get("ip", ""), item.get("hostname", ""), item.get("idc", ""),
item.get("vendor", ""), item.get("model", ""), item.get("role", "")
])
device_detail_data.append([
item.get("sn", ""), item.get("ipv6", ""), item.get("console_ip", ""), item.get("row", ""),
item.get("column", ""), item.get("last_start", ""), item.get("runtime", ""), item.get("image_version", ""),
item.get("over_warrant"), item.get("warrant_time")
])
try:
cursor.executemany(device_sql, device_data)
cursor.executemany(device_detail_sql, device_detail_data)
self.conn.commit()
except Exception as e:
self.conn.rollback()
raise Exception("db insert failed, error: %s" % str(e))
finally:
cursor.close()
def delete(self, data: Dict) -> None:
pass
def update(self, data: Dict) -> None:
pass
def get(self, condition: Optional[Dict] = None) -> List[Device]:
cursor = self.get_conn()
sql = "select ip, hostname, vendor, model, hardware, channel, device_type from devices " \
"join device_detail on devices.sn = device_detail.sn"
where_str = []
if condition is not None:
for k, v in condition.items():
if isinstance(v, int):
where_str.append("%s=%d" % (k, v))
else:
where_str.append("%s='%s'" % (k, v))
if len(where_str) > 0:
sql += (" where %s" % ",".join(where_str))
cursor.execute(sql)
result = cursor.fetchall()
devices = []
for item in result:
devices.append(Device().to_model(**item))
return devices
上述代码中呢,我引入的一个新的第三方库pymysql
,这个库就是Python中用来连接MySQL数据库的最常用的轮子,大多数兼容MySQL的ORM框架也是用pymysql
作为底层驱动。
我们可以直接使用这个库执行原生SQL,虽然我之前有长期使用过这个库,但在编写上述代码的时候仍然会有诸多细节需要仔细确认(我会在视频中详细提到);
相比前面章节使用ORM来操作增删改查,易用性的差距简直不是一点半点;除此之外,由于定义了devices
和device_detail
两张表进行关联,所以大家可以发现,在做数据库操作时会格外的复杂繁琐,如果使用ORM的关联查询的话,处理起这些问题来就非常得心应手。
总结
经过这一章节的讲解,我们已经基本准备好了执行命令必须的几个模块:执行器初始化连接,命令筛选,设备筛选,关于保存和解析部分会在后面章节提到,在阅读完这一章节之后,大家便可以自己尝试一下编写SSHExecutor的代码了。我会在下一章节中手把手带领大家来写SSHExecutor。