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)    收藏  举报

导航