通过前面对Paramiko模块的学习与使用,以及Python中多线程与多进程的了解,依此,就可以开发简易的主机批量管理工具了。

    显然批量管理主机时,程序如果能并发执行功能是最好的,因为这样可以最大程度地利用CPU的性能,因此这就需要使用Python多线程或者多进程,基于学习的需要,这里主要使用多进程来进行开发,当然,这会存在一定问题,后面会说。

    主要内容如下:

1.主机批量管理工具功能
2.设计框架
3.实现:数据库信息与程序源代码
4.实战演示
5.程序的不足
6.在写程序过程中的经验教训
7.往后的改进思路


1.主机批量管理工具功能

    这里的主机主要是指Linux服务器,需要的功能如下:

(1)批量命令执行

    能够通过该程序对管理列表中的主机批量执行管理员输入的命令。

(2)批量文件分发

    对于多台服务器主机需要同一文件时,可以通过该程序远程批量分发指定的文件。

(3)支持自定义端口

    实现(1)(2)的功能都依赖于Paramiko模块,而Paramiko模块是基于SSH来完成的,虽然大多数Linux服务器的SSH端口号都默认使用22,但出于安全的考虑,也有修改默认端口号的情况,比如将SSH远程端口号修改为52113等。

(4)自定义用户

    这里的自定义用户主要是指该程序的用户,把该程序理解为一个批量管理系统,要使用该系统就必然要有该系统的账号与用户名,而每个账号与用户名根据权限的需要,都应该有自己可以管理的主机列表,比如普通运维人员只能管理部分服务器主机,而运维总监则应该可以管理更多的主机,并且他们的管理权限也应该是不一样的,因此,他们分别对应的管理系统的账号的权限就不一样了。

(5)日志记录功能

    运维人员登陆该系统后,对远程服务器主机进行了什么操作、时间、成功与否等信息都要以日志形式记录下来。


2.设计框架

    基于上面几个功能的需要,设计的思路如下:

wKioL1Yfa6SxmBECAAFbZPi37UQ185.jpg


3.实现:数据库信息与程序源代码

    根据需求与设计框架,做如下的工作:

(1)数据库信息

1)管理系统登陆信息数据库

    这里存放的是该系统可以登陆的用户名密码等信息,只有在这里存在的用户名才能进行登陆,如下:

    

创建了manager_system数据库:

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| manager_system     |
| mysql              |
+--------------------+
3 rows in set (0.03 sec)

    

在manager_system数据库中创建了两种类型不同的表:

mysql> use manager_system
mysql> show tables;
+--------------------------+
| Tables_in_manager_system |
+--------------------------+
| manager1_server          |
| manager2_server          |
| users                    |
+--------------------------+
3 rows in set (0.00 sec)

    表users用来存放用户信息,表manager1_server等就是用来存放用户对应的可以管理的主机列表,下面会讲。 


表users就是用来存放系统用户信息的:

mysql> describe users;
+-----------+------------------+------+-----+---------+----------------+
| Field     | Type             | Null | Key | Default | Extra          |
+-----------+------------------+------+-----+---------+----------------+
| id        | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| username  | char(20)         | NO   |     | NULL    |                |
| password  | char(20)         | NO   |     | NULL    |                |
| real_name | char(20)         | NO   |     | NULL    |                |
+-----------+------------------+------+-----+---------+----------------+
4 rows in set (0.01 sec)

    

表users中存放了两个用户信息:

mysql> select * from users;
+----+----------+----------+-----------+
| id | username | password | real_name |
+----+----------+----------+-----------+
|  1 | manager1 | 123456   | zhangsan  |
|  2 | manager2 | 123456   | lisi      |
+----+----------+----------+-----------+
2 rows in set (0.00 sec)

    也就是说,只能用户manager1和manager2才能登陆该系统,其他用户除非向管理员申请注册,否则是无法登陆该系统的。


2)管理系统用户主机列表数据库

    其实还是使用了manager_system的数据库,只是在该数据库中创建了基于用户的不同表,如下:


两种类型不同的表:

mysql> use manager_system
mysql> show tables;
+--------------------------+
| Tables_in_manager_system |
+--------------------------+
| manager1_server          |
| manager2_server          |
| users                    |
+--------------------------+
3 rows in set (0.00 sec)


表[name]_server就是用来存放用户对应的主机列表:

mysql> describe manager1_server;
+-------------+------------------+------+-----+---------+----------------+
| Field       | Type             | Null | Key | Default | Extra          |
+-------------+------------------+------+-----+---------+----------------+
| id          | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| ip          | char(20)         | NO   |     | NULL    |                |
| username    | char(20)         | NO   |     | NULL    |                |
| password    | char(20)         | NO   |     | NULL    |                |
| port        | int(11)          | NO   |     | NULL    |                |
| server_type | char(20)         | NO   |     | NULL    |                |
+-------------+------------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)


表中存放了用户可以管理的主机相关信息:

mysql> select * from manager1_server;
+----+---------------+-----------+----------+-------+-------------+
| id | ip            | username  | password | port  | server_type |
+----+---------------+-----------+----------+-------+-------------+
|  1 | 192.168.1.124 | oldboy    | 123456   |    22 | DNS Server  |
|  2 | 192.168.1.134 | yonghaoye | 123456   | 52113 | DHCP Server |
+----+---------------+-----------+----------+-------+-------------+
2 rows in set (0.00 sec)

    其中这里的ip就是远程主机的IP地址了,server_type就是服务器类型,username和password是远程主机ssh登陆的用户密码,这也说明,只要管理系统用户进入了管理系统,在对远程主机进行管理时,就不需要输入远程主机的用户名和密码了,除了方便外,这也有一定的安全性。

    需要说明的是这里的port端口号,可以看到这里有台主机的port为22,而另一台则为52113,就是前面所说的自定义端口号了,因此,这需要管理员在添加主机时手动指定。


(2)程序源代码

    有了上面的基本数据准备后,再看一下该程序的源代码,其中部分注释会给出,但是基于前面的介绍,代码应该也是比较容易理解的:

import MySQLdb,os,paramiko,sys,time
from multiprocessing import Process,Pool

#数据库连接类
class Connect_mysql:
	conn = MySQLdb.connect(host = 'localhost', user = 'root',passwd = '123456', db = 'manager_system', port = 3306)
	cur = conn.cursor()
	def __init__(self,username,password='NULL'):
		self.username = username
		self.password = password
	#contect to the login table    
	def login_check(self):    #连接管理系统账号信息数据库并验证用户名密码信息
		try:
			self.cur.execute("select * from users where username = '%s' and password = '%s'" % (self.username,self.password))
			qur_result = self.cur.fetchall()	#return the tuple
			
			if qur_result == ():	#database do not have this user
				return 0        
			else:
				return 1 	  		#database has this user
			self.cur.close()
			self.conn.close()

		except MySQLdb.Error,e:
			print '\033[31;1mMysql Error Msg:%s\033[0m' % e
	#contect to the server table
	def return_server(self):    #连接用户主机列表数据库并返回表信息
		self.cur.execute("select * from %s_server" %  self.username)
		qur_result = self.cur.fetchall()
		return qur_result

def ssh_run(host_info,cmd,sysname):    #批量远程命令执行程序
	ip,username,password,port= host_info[1],host_info[2],host_info[3],host_info[4]
	date = time.strftime('%Y_%m_%d')
	date_detial = time.strftime('%Y_%m_%d %H:%M:%S')    
	f = file('./log/%s_%s_record.log' % (sysname,date),'a+')    #操作日志记录,记录程序所有目录的/log目录里
	try:
		s.connect(ip,int(port),username,password,timeout=5)
		stdin,stdout,stderr = s.exec_command(cmd)

		cmd_result = stdout.read(),stderr.read()

		print '\033[32;1m-------------%s--------------\033[0m' % ip
		for line in cmd_result:
			print line,
		print '\033[32;1m-----------------------------\033[0m'
	except:
		log = "Time:%s | Type:%s | Detial:%s | Server:%s | Result:%s\n" % (date_detial,'cmd batch',cmd,ip,'failed')
		f.write(log)
		f.close()
		print '\033[31;1mSomething is wrong of %s\033[0m' % ip
	else:
		log = "Time:%s | Type:%s | Detial:%s | Server:%s | Result:%s\n" % (date_detial,'cmd batch',cmd,ip,'success')
		f.write(log)
		f.close()
		return 1

def distribute_file(host_info,file_name,sysname):    #批量文件分发函数
	ip,username,password,port = host_info[1],host_info[2],host_info[3],int(host_info[4])
	date = time.strftime('%Y_%m_%d')
	date_detial = time.strftime('%Y_%m_%d %H:%M:%S')
	f = file('./log/%s_%s_record.log' % (sysname,date),'a+')    #日志记录
	try:
		t = paramiko.Transport((ip,port))
		t.connect(username=username,password=password)
		sftp = paramiko.SFTPClient.from_transport(t)
		sftp.put(file_name,'/tmp/%s' % file_name)
		t.close()
	except:
		log = "Time:%s | Type:%s | Detial:%s | Server:%s | Result:%s\n" % (date_detial,'distribute file',file_name,ip,'failed')
		f.write(log)
		f.close()
		print '\033[31;1mSomething is wrong of %s\033[0m' % ip
	else:
		log = "Time:%s | Type:%s | Detial:%s | Server:%s | Result:%s\n" % (date_detial,'distribute file',file_name,ip,'success')
		f.write(log)
		f.close()
		print "\033[32;1mDistribute '%s' to %s Successfully!\033[0m" % (file_name,ip)

os.system('clear')
print '\033[32;1mWelcome to the Manager System!\033[0m'

while True:    #程序主程序
	username = raw_input('Username:').strip()
	password = raw_input('Password:').strip()
	if len(username) <= 3 or len(password) < 6:
		print '\033[31;1mInvalid username or password!\033[0m'
		continue
	#Begin to login
	p = Connect_mysql(username,password)
	mark = p.login_check()
	if mark == 0:		#login failed
		print '\033[31;1mUsername or password wrong!Please try again!\033[0m'
	elif mark == 1:		#login success
		print '\033[32;1mLogin Success!\033[0m'
		print 'The server list are as follow:'
		#seek for the server list managed by the system user
		p = Connect_mysql(username)
		server_list = p.return_server()
		for server in server_list:
			print '%s:%s' % (server[5],server[1])
		while True:
			print '''What do you want to do?    #程序主菜单
1.Execute the command batch.
2.Distribute file(s) batch.
3.Exit.'''
			choice = raw_input('\033[32;1mYour choice:\033[0m').strip()
			if '1' <= choice <= '4':pass
			else:continue

			#Execute the command batch.
			if choice == '1':    #批量执行命令程序块
				s = paramiko.SSHClient()    #调用Paramiko模块
				s.load_system_host_keys()
				s.set_missing_host_key_policy(paramiko.AutoAddPolicy())

				p = Pool(processes=3)    #设定进程池数据

				result_list = []
				while True:
					cmd = raw_input('\033[32;0mEnter the command(or quit to quit):\033[0m')
					if cmd == 'quit':break
					for h in server_list:
						result_list.append(p.apply_async(ssh_run,[h,cmd,username]))	#the usename is system name
			#调用相关功能函数,并执行多进程并发
					for res in result_list:
						res.get()
				s.close()

			#Distribute file(s) batch.
			elif choice == '2':    #批量分发文件程序块
				s = paramiko.SSHClient()    #调用Paramiko模块
				s.load_system_host_keys()
				s.set_missing_host_key_policy(paramiko.AutoAddPolicy())

				p = Pool(processes=3)

				result_list = []	#save the suanfa that come from the apply_async
				while True:
					file_name = raw_input('The file you want to distribute(or quit to quit):').strip()
					start = time.time()
					if file_name == 'quit':break
					file_chcek = os.path.isfile(file_name)
					log_list = []
					if file_chcek == False:
						print '\033[31;1mThe file does not exist or it is a directory!\033[0m'
						continue
					for h in server_list:
						result_list.append(p.apply_async(distribute_file,[h,file_name,username]))	#the list save the suanfa
					for res in result_list:
						res.get()	#run the suanfa
					end = time.time()
					print '\033[31;1mCost time:%ss\033[0m' % str(end - start)
				s.close()

			#Exit the system
			elif choice == '3':    #退出系统
				sys.exit('\033[32;1mWelcome to use our system!\033[0m')

    程序的代码量不多,主要功能也有注释,只要的paramiko模拟的SSH命令执行与SFTP文件分发、Python多进程以及进程池的使用有所了解,还是比较容易理解的。


4.实战演示

    基于几个主要功能:批量命令执行、文件分发、日志记录,下面来做个演示:

(1)登陆系统

wKiom1Yfc-Sii0GxAARPEGGrpFs475.jpg

(2)批量执行命令

wKiom1YfdIbAm-3lAAQ2eJ7xjhs060.jpg


(3)批量分发文件

wKioL1YfdVigCpPHAAMOTwyNdPo417.jpg


(4)退出系统

wKiom1YfdYmiXwXlAAFt2u8hBGk738.jpg


(5)用户操作日志查看

查看日志文件列表:

wKiom1YfdivBZgWQAAE_WBjtjsM572.jpg    

    可以看到,操作日志是基于不同用户的(这里会有manager2的日志文件是我后来用manager2登陆后执行操作产生的,上面没有给出manager2的操作过程),并且是按天生成的,只要用户登陆并执行相关操作,日志文件是自动生成的。


查看详细日志内容:

wKiom1YfdtfygGy0AALGVWwXkSo569.jpg    

    可以查看该用户在具体时间所做的具体操作,以及成功与否等更细节的信息。


5.程序的不足

(1)细节上的问题

    由于未投入生产使用,所以会存在一些意想不到的Bug,主要是在连接远程主机时可能引发的各种异常。

(2)使用了多进程

    虽然使用了进程池了限制同一时间内并发的进程数,但仍然不可避免会出现文件资源抢占的情况,只是这里因为并发的进程只有两个,所以影响并不会很大,基于该程序比较小型,因此往后也可继续使用多进程,当然,使用多线程效果会更好,这里只是出于学习的需要而使用多进程。

(3)程序代码简洁程序

    程序的代码简洁程度可以进一步优化,比如两个功能:批量执行命令和分发文件,其实里面就有很多重复的代码,本来是考虑用类的方法来重写的,但是对面向对象编程又不够熟悉,所以就没有使用了。

    当然,这其实只是非常小的的一个程序,不过倒是可以作为以后开发特定功能的监控系统时的某一模块来使用,不管怎么说,框架在这里的话应该还是正确的,由于时间和实际需要情况的考虑,在这里并没有做太多的界面优化,因为以后是考虑做成Web界面的。当然,就从学习Python的角度来考虑,这个程序还是可以练练手,各种小的积累才能沉淀出优秀的大型开源软件。


6.在写程序过程中的经验教训

    一开始函数有参数file作为我传入的文件字,但是函数中又使用了file来打开文件,所以执行程序时一直显示TypeError: 'str' object is not callable的错误,后来无意将file改变open发现又正常了,于是才很意识到这个错误。如果一开始就有这个意识,后面也不会浪费这么多时间,所以这一点要尤其注意了。


7.往后的改进思路

(1)在以后学习了Django的相关知识后,可以考虑将其写成是基于Web界面的管理系统。

(2)加强测试,尽可能找出程序中低级的Bug。

(3)改用多线程来写程序,并注意文件资源的抢占问题。

(4)将功能重复的代码用函数方式或面向对象编程方式来重写。

    

    分享给大家,希望对同在Python学习路上的新手带来实际性的帮助!