MSCHED实现(一):
自动化运维平台:saltstack,ansible。
这些平台二次开发的难度大:1.需要了解它的整体架构、设计思想、熟悉大部分代码,2.维护成本很高,如果原有平台的版本升级,将更困难。如果只是某个第三方库的修改,无所谓。
如果要对原平台进行二次开发,1.基于现有产品,封装一个web界面,2.更深层:改代码,回馈给社区,以便新版本时,不需要自己去独立维护。
自动化程度高的大公司,一般都是自己实现一个平台。
分发任务、执行任务的两种方式:有agent、无agent。
无agent,指的是有一个通用的agent,如sshd。其缺点:当大规模时并发效率不高、权限问题、ssh连接是有状态的。
有agent,指的是自己写的agent。
下面将自己来实现一个简单的自动化运维平台,目标:分发任务、控制任务(并行/错误处理/终止)、跨机房部署,能适应大多数反复执行的运维任务;这里,实际执行任务是有agent,不和master通讯;agent的安装实始化采用无agent。
如何让python执行一个脚本,引入subprocess的Popen方法。
import subprocess class Executor: def __init__(self,script): self.script = script def run(self): p = subprocess.Popen(self.script, shell=True) p.wait() if __name__ == '__main__': executor = Executor('touch /root/workspace/msched/tmp') executor.run()
流程:用户将任务告知master,master把任务分发给agent,agent执行任务。
任务描述:1.script,2.目标节点,3.并行方式,4.允许的错误数,5.超时时间,6.参数(常见的:环境变量、命令行参数),参数是容易逻辑出错和人为出错的地方,so参数的实现要谨慎、健壮、侦测...
任务描述中的script有各种字符,统一其的方法,使用base64
通讯协议的选择:用户和master之间通讯使用http协议;master和agent之间的通讯协议选择很多,如:websocket,stomp,http2,AMQP等,这里选择tcp,master需要知道agent的心跳信息。
消息:agent为客户端,要有全局唯一标识,向master服务端发送注册消息,心跳消息(flag(当前agent是否有任务在执行))、任务结果消息。任务消息由master向agent发送,它需要任务描述,而脚本script字符串中包含各种各样的符号,需要做一个统一的编码,通常使用base64,以确保脚本中没有特殊的符号,可以正常通讯。
一、消息:
在项目msched下,创建message目录,在message目录下创建:注册消息register.json、心跳消息heartbeat.json、任务消息task.json、结果消息result.json。任务消息中的script脚本是经过base64编码的
{ "type": "register", "payload": { "id": "", "hostname": "", "ip": [] } }
{ "type": "heartbeat", "payload": { "id": "", "hostname": "", "ip": [], "busy": false } }
{ "type": "task", "payload": { "id": "", "script": "", "timeout": 0, "parallel": 1, "fail_count": -1 } }
{ "type": "result", "payload": {
"agent_id": "", "task_id": "", "code": 0, "output": "" } }
二、agent,在项目msched下创建python package:agent目录
在agent目录下,创建连接管理程序cm.py,功能:消息转码及发送注册消息、发送心跳消息、接收任务消息、发送任务结果消息:
import socket
import threading
import json
from .msg import Messages
from .executor import Executor
class ConnectionManager:
def __init__(self, master, message: Messages):
self.master = master
self.so = socket.socket()
self.message = message
self.event = threading.Event()
def _encode(self, msg):
return '{}\r\n'.format(msg).encode()
def _heartbeat(self): # send heartbeat message once 3
while not self.event.is_set():
self.so.send(self._encode(self.message.hearbeat()))
self.event.wait(3)
def _recv(self): # receive task message
f = self.so.makefile()
while not self.event.is_set():
msg = f.readline()
message = json.loads(msg)
if message.get('type') == 'task':
self.message.busy = True
code, ouput = Executor.run(message['payload'])
self.so.send(self._encode(self.message.result(code, output)))
self.message.busy = False
def start(self):
self.so.connect(self.master)
self.so.send(self._encode(self.message.register()))
threading.Thread(name='heartbeat', target=self._heartbeat, daemon=True).start()
threading.Thread(name='recv', target=self._recv, daemon=True).start().start()
def shutdown(self):
self.event.set()
self.so.close()
创建消息收集器msg.py,功能:收集注册消息、收集心跳消息和收集任务结果消息
import os import uuid import netifaces import ipaddress class Messages: def __init__(self, path): if os.path.exists(path): with open(path) as f: self.id = f.readline() self.id == uuid.uuid4().hex with open(path, 'w') as f: f.write(self.id) self.busy = False def _get_address(self): address = [] for iface in netifaces.interfaces(): # iface: get all pysical interfaces for nets in netifaces.ifaddresses(iface).values(): # nets/net: get address of each interface, include ip address for net in nets: addr = ipaddress.ip_address(net['addr']) # get ip from nets/net addresses if addr.is_loopback: continue if addr.is_link_local: continue if addr.is_multicast: continue address.append(str(addr)) return address def register(self): return { 'type': 'register', 'payload': { 'id': self.id, 'hostname': os.uname().nodename, 'ip': self._get_address() } } def hearbeat(self): return { 'type': 'heartbeat', 'payload': { 'id': self.id, 'hostname': os.uname().nodename, 'ip': self._get_address(), 'busy': self.busy } } def result(self, code, output): return { 'type': 'result', 'payload': { 'id': self.id, 'code': code, 'output': output } }
修改executor.py,功能:对脚本script转码及执行
import base64 import subprocess class Executor: @classmethod def run(cls, task): script = base64.b64decode(task['script']) p = subprocess.Popen(script, shell=True,stdout=subprocess.PIPE, stderr=subprocess.STDOUT) p.wait(task.get('timeout', 0)) return p.returncode, p.stdout.read()
agent部分尽量简单,减少bug,减少维护量。
三、重连,是比较困难的一部分。
短连接,不存在重连。长连接,当连接断开了,如何重新连接?1.尝试连接,将connect和send方法独立出来,try之;2.使用event,控制while循环。编辑cm.py:
import socket import threading import json import logging from .msg import Messages
from .executor import Executor class ConnectManager: def __add__(self, master, message: Messages): self.so = socket.socket() self.master = master self.message = message self.event = threading.Event() def _encode(self, msg): return '{}\r\n'.format(msg).encode() def _heartbeat(self): while not self.event.is_set() : self._send(self.message.heatbeat()) self.event.wait(3) def _recv(self): while not self.event.is_set(): try: with self.so.makefile() as f: msg = f.readline() message = json.loads(msg) if message.get('type') == 'task': self.message.busy = True code, output = Exception.run(message['payload']) self._send(self.message.result(code, output)) self.message.busy = False except Exception as e: pass def _send(self, msg): while not self.event.is_set(): try: self.so.send(self._encode(msg)) break except Exception as e: logging.error('send message error: {}'.format(e)) self.so.close() self._connect() def _connect(self): while not self.event.is_set(): try: self.so.connect(self.master) break except Exception as e:
self.so = socket.socket() # 即使socket断开了,重新连接,也不能重复使用原来的端口 logging.error('connect to master {}:{} error: {}'.format(*self.master, e)) self.event.wait(3) def start(self): self._connect() self._send(self.message.register()) threading.Thread(name='heartbeat', target=self._heartbeat, daemon=True).start() threading.Thread(name='result', target=self._recv, daemon=True).start() def shutdown(self): self.event.set() self.so.close()
socket有个问题,在重新定义socket的时侯,端口不能重复使用,重连将遇到此问题。如何解决,对连接加锁lock:
def _send(self, msg): while not self.event.is_set(): try: with self.lock: self.so.send(self._encode(msg)) break except Exception as e: logging.error('send message error: {}'.format(e)) self._connect() def _connect(self): while not self.event.is_set(): with self.lock: try: self.so.connect(self.master) break except Exception as e: self.so.close() self.so = socket.socket() logging.error('connect to master {}:{} error: {}'.fromat(*self.master, e)) self.event.wait(3)
posted on 2017-05-21 17:22 myworldworld 阅读(273) 评论(0) 收藏 举报
浙公网安备 33010602011771号