一 概述
1 運維管理的階段
1 人工階段
人工盯着服務器,出了問題,到機器前面,翻日誌,查狀態,手動操作
2 腳本階段
開始寫一些自動化腳本,啓動計劃任務,自動啓動服務,監控服務等
3 工具階段
腳本功能太弱,開發了大量工具,某種工具解決某個特定領域的問題,常用的有ansible,puppet等
4 平臺階段
將工具整合,自主研發,實現標準化,實現自動化流程控制,而今,平臺已經開始邁向智能化的發展方向。
二 mschedule 設計
1 完整代碼鏈接
2要求
1 分發任務
分發腳本到目前節點上去執行
2 控制
控制併發,控制多少個節點同時執行
對錯誤做出響應,由用戶設定,最多允許失敗的比例或者數量,當超過範圍時,需要終止任務執行
3 能跨機房部署
4 能對作業做版本控制,這是輔助功能,可過後實現
3 項目基本概述
1 基本概述
本項目的出發點,是只需要會使用shell腳本就可以了,可以通過使用shell腳本的方式來完成遠程任務的下發和處理流程。
2 其他自動化工具二次開發缺點
ansible,salt等需要學習特定的內部語言,如果覺得ansible這樣的工具不能滿足需求,二次開發難度過高,代碼量不小,本身它們開發接口不完善,而且熟悉它的叫也比較難,就算開發出來維護也難。
從這些項目上二次開發,等於拉一個分支,如果主分支有了新的特性,想要合併也是比較困難的。
自己開發,滿足自己需求,完全適合自己需求,代碼規模可控,便於他人接收維護。
3 項目初始版本目標
自己開發就是造輪子,造輪子不是不好,其起初要實現的功能應該是比較簡單的。後面可以逐步進行完善操作。
4 項目基本架構圖
瀏覽器端和webSERVER端交互是通過HTTP實現的,而WEB server和master server 是通過TCP鏈接來實現的,master server 和agent之間也是通過TCP 鏈接來實現的
4 分發任務設計
1 分發任務分類
1 有agent 類
有agent類,被控節點需要安裝或運行特殊的軟件,用於和服務器端進行通信,服務器端把腳本,命令傳遞給agent端,由agent端控制來執行
2 無agent類
被控節點不需要安裝或者運行特殊軟件,如通過SSH來實現,這其實也是有agent的,不過不是自己寫的程序
優缺點
1 通用,簡單,易實現,但管理不善,容易出現安全問題
2 並行效率不高,有agent的並行執行可以不和管理服務器通信,可以併發很高,ssh執行要和master之間通信
3 ssh鏈接是有狀態的,任務執行的時候,master不能掛了,否則任務將執行失敗。
5 執行腳本(subprocess)
python 中有很多運行進程的方式,不過都過時了。
建議使用標準庫subprocess模塊,啓動一個子進程。
1 初始化類源碼
def __init__(self, args, bufsize=-1, executable=None,
stdin=None, stdout=None, stderr=None,
preexec_fn=None, close_fds=_PLATFORM_DEFAULT_CLOSE_FDS,
shell=False, cwd=None, env=None, universal_newlines=False,
startupinfo=None, creationflags=0,
restore_signals=True, start_new_session=False,
pass_fds=()):
第一個是參數,後面是可選,但shell默認爲False,可將其置爲True, stdout 後面跟文件或管道
def wait(self, timeout=None, endtime=None):
"""Wait for child process to terminate. Returns returncode
attribute."""
if endtime is not None:
timeout = self._remaining_time(endtime)
if timeout is None:
timeout_millis = _winapi.INFINITE
else:
timeout_millis = int(timeout * 1000)
if self.returncode is None:
result = _winapi.WaitForSingleObject(self._handle,
timeout_millis)
if result == _winapi.WAIT_TIMEOUT:
raise TimeoutExpired(self.args, timeout)
self.returncode = _winapi.GetExitCodeProcess(self._handle)
return self.returncode
此處返回是狀態,0爲成功,其他爲失敗
stdout 方法調用的是一個文件,因此可使用文件的形式進行處理
if c2pread != -1:
self.stdout = io.open(c2pread, 'rb', bufsize)
if universal_newlines:
self.stdout = io.TextIOWrapper(self.stdout)
2 基本代碼如下
#!/usr/bin/poython3.6
#conding:utf-8
import subprocess
from subprocess import Popen,PIPE
out=Popen("echo 'hello'",shell=True,stdout=PIPE)
code=out.wait(10)
txt=out.stdout.read()
print ("code={} txt={}".format(code,txt.decode()))
結果如下
6 項目基本構建
1 創建文件並添加虛擬環境
mkdir mschedule -p
cd mschedule/
pyenv virtualenv 3.5.3 msch
pyenv local msch
2 構建模塊agent,並創建執行程序executor.py
#!/usr/bin/poython3.6
#conding:utf-8
from subprocess import PIPE,Popen
class Executor:
def run(self,script,timeout):
p=Popen(script,shell=True,stdout=PIPE)
code=p.wait(timeout=timeout)
txt=p.stdout.read()
return (code,txt)
if __name__ == "__main__":
exec=Executor()
print (exec.run("echo 'hello'",3))
結果如下
7 agent 和master設計
用戶和master server 通信,提交任務,此處是通過HTTP的方式提交任務
master 按照用戶要求將任務分發到指定的節點上,這些節點上需要有agent用於和master通信,接受master發佈的任務,並執行這些任務
設計agent,越簡單越好,越簡單bug越少,越穩定。
從本質上來說,master,agent設計是典型的CS編程模式
master作爲CS中的server,agent作爲CS中的client
8 消息設計
1 註冊信息
agent啓動後,需要主動連接server,並註冊自己
信息包括
hostname:報告自己的主機名稱,此主機名稱可能會重複UUID,用於唯一標識這臺主機
IP: 用於更加方便的管理主機
其它相關信息視情況而定
{
"type": "register", # 此處用於定義消息類型
"payload":{
"id" : uuid, #用於唯一標識一臺主機
"hostname": "xxxx", # 對應agent名稱
"IP": [], # agent IP地址,其可能包含多個IP地址,因此此處使用列表進行存儲
}
}
2 心跳信息
agent定時向master發送心跳包,包含UUID這個唯一標識,附帶hostname和ip地址,hostname和ip都可能變動,但agent不變,其UUID便不會發生變化,其他相關信息科一附加, 如更加flag,用於標識agent是否有正在執行的任務。
{
"type": "heartbeat", # 此處用於定義消息類型
"payload":{
"id" : uuid, #用於唯一標識一臺主機
"hostname": "xxxx", # 對應agent名稱
"IP": [], # agent IP地址,其可能包含多個IP地址,因此此處使用列表進行存儲
}
}
3 任務消息
master分派任務給agent,發送任務描述信息到agent。
注意腳本字符串使用base64編碼
{
"type" :"task",
"payload" :{
"id" :"task-uuid", # 定義任務的唯一標識
"script" : "base64code", #定義執行任務的內容
"timeout" :0, # 定義超時時長
"parallel" :1, # 定義並行執行數
"fail_rate" :0, # 定義失敗率,及百分比爲多少代表失敗
"fail_count" :-1 # 定義失敗的次數爲多少次表示失敗,-1表示不關心
}
}
4 任務結果消息
當agent任務執行完成後,返回給master該任務執行的狀態碼和輸出結果。
{
"type" :"result",
"payload" :{
"id": "task-uuid", # 定義任務唯一標識
"agent_id": "agent-uuid", #定義任務執行者
"code" : 0, #定義任務執行結果返回值。0 表示成功,其他表示失敗
"output" :"base64encode" # 定義任務執行結果,及輸出到控制檯的結果
}
}
以上的master,agent之間需要傳遞消息,消息採用json格式。
三 agent端代碼實現
1 日誌實現
具體代碼如下
#!/usr/bin/poython3.6
#conding:utf-8
import logging
def getlogger(mod_name:str,filepath:str='/var/log/mschedule'):
logger=logging.getLogger(mod_name) # 獲取名字
logger.setLevel(logging.INFO) # 添加日誌級別
logger.propagate=False # 配置不想上傳遞
handler=logging.FileHandler("{}/{}.log".format(filepath,mod_name))
fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s (%(filename)s:L%(lineno)d)",
datefmt='%Y-%m-%d %H:%M:%S')
handler.setFormatter(fmt)
logger.addHandler(handler)
return logger
if __name__ == "__main__":
log = getlogger('test')
log.info('13234545654')
結果如下
2 通信模塊實現(zerorpc )
1 介紹和安裝
原生的socket編程過於底層,很少使用,任何一門語言都要避開直接使用socket庫開發,太過底層,難寫難維護。
zeroprc 是基於 ZeroMQ和MessagePack 來實現的通信工具。
官網地址
http://www.zerorpc.io
安裝
pip install zerorpc
2 基本代碼實現
根目錄創建app.py和appserver.py
server 端配置
#!/usr/bin/poython3.6
#conding:utf-8
import zerorpc
class HelloRPC(object): #定義方法
def hello(self, name):
return "Hello, %s" % name
s = zerorpc.Server(HelloRPC()) # 方法注入
s.bind("tcp://0.0.0.0:8080") # 綁定方法
s.run() # 運行方法
client端配置
#!/usr/bin/poython3.6
#conding:utf-8
import zerorpc
c = zerorpc.Client()
c.connect("tcp://127.0.0.1:8080")
print (c.hello("RPC"))
#!/usr/bin/poython3.6
#conding:utf-8
import zerorpc
import threading
c = zerorpc.Client()
c.connect("tcp://127.0.0.1:8080")
e=threading.Event()
while not e.wait(3):
print(c.hello('test client'))
print ('```````````````')
結果如下
3 註冊消息實現
1 uuid唯一主機標識
使用uuid.uuid4().hex 獲取一個uuid,一個節點起始運行的時候是沒有uuid的,一旦運行會生成一個uuid,並持久化到一個文件中,下次運行先找這個文件,如果文件中有uuid,就直接讀取,沒有uuid就重新生成並寫入到該文件中。
#!/usr/bin/poython3.6
#conding:utf-8
#!/usr/bin/poython3.6
#conding:utf-8
import uuid
print (uuid.uuid4().hex)
print (uuid.uuid4().hex)
print (uuid.uuid4().hex)
結果如下
2 hostname
windows 和Linux 獲取主機名稱的方式是不同的
可以在所有平臺上是使用socket.gethostname()獲取主機名。
#!/usr/bin/poython3.6
#conding:utf-8
import socket
print (socket.gethostname())
3 ip 列表
pip install netifaces
netifaces.interfaces() 返回接口列表
netifaces.ifaddresss(interface) 獲取指定接口的IP地址,返回相關信息
ip地址判斷
#!/usr/bin/poython3.6
#conding:utf-8
import ipaddress
ips=['127.0.0.1','192.168.0.1','169.254.123.1','0.0.0.0','239.168.0.255','224.0.0.1','8.8.8.8']
for ip in ips:
print (ip)
ip=ipaddress.ip_address(ip)
print ('Linklocal {}'.format(ip.is_link_local)) # 169.254地址
print ('迴環 {}'.format(ip.is_loopback)) # 迴環
print ('多播 {}'.format(ip.is_multicast)) # 多播
print ('公網 {}'.format(ip.is_global)) # 公網,全球範圍地址
print ('私有 {}'.format(ip.is_private)) # 私有地址
print ('保留 {}'.format(ip.is_reserved)) # 保留地址
print ('版本 {}'.format(ip.version)) #ipv4地址
print ('----------------------------')
結果如下
#!/usr/bin/poython3.6
#conding:utf-8
import netifaces
print (netifaces.interfaces()) # 獲取所有的網卡接口
for i in netifaces.interfaces():
print ('i....',netifaces.ifaddresses(i)) # 使用ifaddress獲取端口對應的IP地址
print ()
print ('------------------------------')
print ()
print ('[2]',netifaces.ifaddresses(i)[2]) # 獲取字典key爲2的對應的值
結果如下
其是一個字典,key爲2就是ipv4地址
每一個接口返回的ipv4地址是一個列表,也就是說可以有多個,ipv4地址描述是在addr上
#!/usr/bin/poython3.6
#conding:utf-8
import netifaces
print (netifaces.interfaces()) # 獲取所有的網卡接口
for i in netifaces.interfaces():
for p in netifaces.ifaddresses(i)[2]:
if p['addr']:
print ('ip',p['addr']) # 獲取ip地址
結果如下
#!/usr/bin/poython3.6
#conding:utf-8
import netifaces
import ipaddress
print (netifaces.interfaces()) # 獲取所有的網卡接口
for i in netifaces.interfaces():
for p in netifaces.ifaddresses(i)[2]:
if p['addr']:
ip=ipaddress.ip_address(p['addr']) #獲取ip地址
if ip.is_loopback or ip.is_multicast or ip.is_link_local or ip.is_reserved: # 判斷IP地址
continue
print (ip)
結果如下
4 註冊信息和相關信息處理
在agent文件包中創建msg.py文件,用於存儲相關主從信息和配置信息
#!/usr/bin/poython3.6
#conding:utf-8
import socket
import uuid
import netifaces
import ipaddress
import os
class Messgae:
def __init__(self,myidpath):
if os.path.exists(myidpath): # 如果存在
with open(myidpath) as f:
self.id=f.readline().strip()
else:
self.id=uuid.uuid4().hex
with open(myidpath,'w') as f:
f.write(self.id)
def get_ipaddress(self):
address=[]
for p in netifaces.interfaces(): # 獲取網口列表
n=netifaces.ifaddresses(p) # 獲取字典
if n.get(2): # 查看是否存在ipv4地址
for ip in n[2]: # 此處獲取對應列表的值
if ip['addr']: # 查看ip地址是否存在
ip=ipaddress.ip_address(ip['addr'])
if ip.is_reserved or ip.is_multicast or ip.is_link_local or ip.is_loopback:
continue
address.append(str(ip))
return address
def hearbeat(self):
return {
"type" :"hearbeat",
"payload" :{
"ip" : self.get_ipaddress(),
"hostname" : socket.gethostname(),
"id" : self.id
}
}
def reg(self):
return {
"type" :"register",
"payload" :{
"ip" : self.get_ipaddress(),
"hostname" : socket.gethostname(),
"id" : self.id
}
}
if __name__ == "__main__":
msg=Messgae('/var/log/mschedule/uuid')
print (msg.reg())
測試結果如下
5 處理鏈接相關配置
agent中創建config模塊用於添加相關鏈接服務端IP地址
agent中創建cm 模塊用於處理鏈接相關配置
config.py 配置如下
#!/usr/bin/poython3.6
#conding:utf-8
CONN_URL="tcp://127.0.0.1:9000"
cm.py 模塊配置如下
#!/usr/bin/poython3.6
#conding:utf-8
import zerorpc #添加模塊
import threading # 用於處理中斷相關
from .msg import Messgae # 獲取消息
from .config import CONN_URL
from utils import getlogger
class Conn_Manager:
def __init__(self,timeout=3):
self.timeout=timeout
self.client=zerorpc.Client()
self.event=threading.Event()
self.message=Messgae('/var/log/mschedule/uuid') # 此處用於初始化消息
self.log=getlogger('agent') # 此處填寫相關的log日誌名稱
def start(self):
self.client.connect(CONN_URL) # 鏈接處理
self.log.info('註冊消息發送 {}'.format(self.client.send(self.message.reg()))) # 發送心跳信息
self.client.send(self.message.reg()) #處理註冊消息
while not self.event.wait(self.timeout): # 等待的時間
self.log.info('心跳消息發送 {}'.format(self.client.send(self.message.hearbeat()))) # 發送心跳信息
def shutdown(self):
self.log.info("關閉操作")
self.client.close()
self.event.set()
agent 中 _init_.py 端配置
#!/usr/bin/poython3.6
#conding:utf-8
from .cm import Conn_Manager
class app:
def __init__(self,timeout):
self.conn=Conn_Manager(timeout)
def start(self):
self.conn.start()
def shutdown(self):
self.conn.shutdown()
全局根目錄下 app.py 端配置如下
#!/usr/bin/poython3.6
#conding:utf-8
from agent import app
if __name__ == "__main__":
agent=app(3)
try:
agent.start()
except KeyboardInterrupt:
agent.shutdown()
服務端測試文件appserver 配置如下
#!/usr/bin/poython3.6
#conding:utf-8
import zerorpc
class HelloRPC(object): #定義方法
def send(self, name):
return "Hello, %s" % name
s = zerorpc.Server(HelloRPC()) # 方法注入
s.bind("tcp://0.0.0.0:9000") # 綁定方法
s.run() # 運行方法
啓動結果如下
日誌結果如下
處理客戶端重連機制
默認的,服務端關閉後,客戶端結果如下
處理結果如下
cm.py如下
#!/usr/bin/poython3.6
#conding:utf-8
import zerorpc #添加模塊
import threading # 用於處理中斷相關
from .msg import Messgae # 獲取消息
from .config import CONN_URL
from utils import getlogger
class Conn_Manager:
def __init__(self,timeout=3):
self.timeout=timeout
self.client=zerorpc.Client()
self.event=threading.Event()
self.message=Messgae('/var/log/mschedule/uuid') # 此處用於初始化消息
self.log=getlogger('agent') # 此處填寫相關的log日誌名稱
def start(self):
try:
self.client.connect(CONN_URL) # 鏈接處理
self.log.info('註冊消息發送 {}'.format(self.client.send(self.message.reg()))) # 發送心跳信息
self.client.send(self.message.reg()) #處理註冊消息
while not self.event.wait(self.timeout): # 等待的時間
self.log.info('心跳消息發送 {}'.format(self.client.send(self.message.hreadbeat()))) # 發送心跳信息
except Exception as e:
print ('--------------------')
self.event.set()
raise e # 此處是拋出異常到上一級
def shutdown(self):
self.log.info("關閉操作")
self.client.close()
self.event.set()
agent._init_.py 結果如下
#!/usr/bin/poython3.6
#conding:utf-8
from .cm import Conn_Manager
import threading
class app:
def __init__(self,timeout):
self.conn=Conn_Manager(timeout)
self.event=threading.Event()
def start(self):
while not self.event.is_set():
try:
self.conn.start()
except Exception as e:
print('重連')
self.conn.shutdown()
self.event.wait(3)
def shutdown(self):
self.event.set()
self.conn.shutdown()
app.py 如下
#!/usr/bin/poython3.6
#conding:utf-8
from agent import app
if __name__ == "__main__":
agent=app(3)
try:
agent.start()
except KeyboardInterrupt:
agent.shutdown()
結果如下
四 master端實現
1 基本功能
1 TCP Server
綁定端口,啓動監聽,等待agent鏈接。
2 信息存儲
存儲agent列表
存儲用戶提交的Task列表,用戶通過WEB提交的任務信息存儲下來。
3 接受註冊
將註冊信息寫入agent列表
接受心跳信息
接受agent端發送的心跳信息
4 派發任務
將用戶提交的任務分配到agent端
2 基本代碼實現
1 master.config 模塊
用於指定服務端綁定IP地址和端口號
#!/usr/bin/poython3.6
#conding:utf-8
MASTER_URL="tcp://0.0.0.0:9000"
if __name__ == "__main__":
pass
2 master.handler 模塊
主要負責客戶端數據的調度
#!/usr/bin/poython3.6
#conding:utf-8
from utils import getlogger
log=getlogger('handler')
class Handler(object):
def send(self,msg): # 定義一個可調用的基礎函數
log.info(" ack ok {}".format(msg))
return " ack ok {}".format(msg)
3 cm.py 模塊
用於tcp 鏈接建立和關閉
#!/usr/bin/poython3.6
#conding:utf-8
from utils import getlogger
from .config import MASTER_URL
import zerorpc
from .handler import Handler
log=getlogger('server')
class Master_Listen:
def __init__(self):
self.server=zerorpc.Server(Handler())
def start(self):
self.server.bind(MASTER_URL)
log.info('Master 啓動配置')
self.server.run()
def shutdown(self):
self.server.close()
4 master._init_.py 模塊
#!/usr/bin/poython3.6
#conding:utf-8
from .cm import Master_Listen
class appserver:
def __init__(self):
self.appserver=Master_Listen()
def start(self):
self.appserver.start()
def shutdown(self):
self.appserver.shutdown()
5 appserver.py模塊
#!/usr/bin/poython3.6
#conding:utf-8
from master import appserver
if __name__ == "__main__":
appserver=appserver()
try:
appserver.start()
except KeyboardInterrupt:
appserver.shutdown()
啓動服務測試如下
結果如下
上述代碼實現了基本的註冊,心跳部分的功能
經觀察可知,目前註冊和心跳除了類型不同外,其可以認爲第一次心跳成功就是註冊。
3 master的數據設計
master端核心需要存儲2中數據:agent端數據,用戶客戶端瀏覽器提交的任務Task,構造出一個數據結構,存儲相關信息.具體數據結構如下
1 agent客戶端數據存儲結構
{
"agents" :{
"agent_id" :{
"heartbeat" :"timestamp",
"busy" :False,
"info" :{
"hostname" :"",
"ip" :[]
}
}
}
}
數據結構解釋如下
1 agents裏面記錄了所有註冊的agent
agent_id,字典的key,每一個agent 都有一個不同uuid,所以這個字典的鍵就是uuid,
heartbeat 由於設計中並沒有讓agent端發送心跳時間,所以就在master端記錄了收到的時間
busy 如果agent 上有任務在執行。則此值表現爲True
info 記錄agent上發過來的hostname和ip列表
2 task數據存儲結構
{
"tasks" :{
"task_id" :{
"script" :"base64encode",
"targets" :{
"agent_id" :{
"state":"WAITING",
"output" :""
}
},
"state" :"WAITING"
}
}
}
task 記錄所有任務及target(agent)的狀態
task_id ,字典的key對應一個一個task,item 也是taskid:{} 結構
task 任務,task.json 的payload信息
targets目標,用於指定agent的節點,記錄agent上的state和輸出output
state狀態,單個agent上的執行狀態state 這是一個task的狀態,整個任務的狀態,比如統計達到了agent失敗上限了,這個task的state 就置爲失敗
狀態常量
"WAITING" "RUNNING" "SUCCEED" "FAILED"
4 agent 端信息存儲
創建 storage.py 模塊
構建Storage 類,用於存儲用戶信息
#!/usr/bin/poython3.6
#conding:utf-8
import datetime
class Storage:
def __init__(self):
self.agents={} # 此處用於存儲用戶信息
self.tasks={} # 此處用於存儲作業信息
def reg_hb(self,agent_id,info): # id 及就是客戶端的id ,info 及就是host和ip地址
self.agents[agent_id] = {
'heaerbeat' : datetime.datetime.now(),
'info' :info,
'busy':self.agents.get(agent_id,{}).get('busy',False)
}
# busy 讀不到置False,讀到了不變
handler.py端配置如下
#!/usr/bin/poython3.6
#conding:utf-8
from utils import getlogger
from .storage import Storage
log=getlogger('handler')
class Handler(object):
def __init__(self):
self.store=Storage()
def send(self,msg): # 定義一個可調用的基礎函數,此處的msg及就是對應的函數
log.info('客戶端agent發送消息爲:{}'.format(msg))
try:
if msg['type'] in {'hearbeat','register'}:
payload=msg['payload']
info={'hostname' :payload['hostname'],'ip' :payload['ip']}
self.store.reg_hb(payload['id'],info)
log.info("客戶端數據列表爲:{}".format(self.store.agents)) # 客戶端的列表
return "agent信息爲: {}".format(msg)
except Exception as e:
log.error("註冊客戶端信息錯誤爲:{}".format(e))
return "Bad Request...."
運行結果如下
5 task 任務基本註冊和創建
1 概述
用戶通過WEB(HTTP)提交新的任務,任務json信息有:
1 任務腳本script,base64編碼
2 超時時間timeout
3 並行度 parallel
4 失敗率 fail_rate
5 失敗次數fail_count
6 targets 是跑任務的Agent的agent_id列表,這個目前也是在用戶端選好的,如yoghurt需要在主機名爲webserver-xxxx的幾臺設備上運行腳本,爲了用戶方便,可以使用類似ansible的分組。在Master端受到信息後,需要添加2個信息
task_id 是Mater 端新建任務時生成的uuid
state 默認狀態是WAITING
在WEB server 中最後將用戶端發送來的數據組成下面的字典
task={
"task_id" :t.id,
"script" :t.script,
"timeout":t.timeout,
"parallel" :t.parallelm,
"fail_rate":t.fail_rate,
"fail_count":t.fail_count,
"state":t.state,
"targets":t.targets
}
2 構建state類
用於處理相關消息的類型
#!/usr/bin/poython3.6
#conding:utf-8
WAITING='WAITING'
RUNNING='RUNNING'
SUCCEED='SUCCEED'
FAILED='FAILED'
3 構建task類
創建master/task.py 類處理webserver端數據
\
#!/usr/bin/poython3.6
#conding:utf-8
import uuid # 獲取唯一的task_id
from .state import *
class Task:
def __init__(self,task_id,script,targets,timeout=0,parallel=1,fail_rate=0,fail_count=-1):
self.id=task_id # task唯一標識,用於確定任務
self.script=script # 對應的腳本內容,客戶端輸入的腳本
self.timeout=timeout # 超時時間
self.parallel=parallel # 並行執行數量
self.fail_rate=fail_rate #失敗率
self.fail_count=fail_count #失敗數
self.state=WAITING # 對應的消息的狀態
self.targets={agent_id:{'state' : WAITING,'output':''} for agent_id in targets} # 此處對應客戶端列表
self.target_count=len(self.targets) # 此處對應客戶端的數量
在master.storage.py模塊中進行相關方法調用,並將其存儲進入task中
#!/usr/bin/poython3.6
#conding:utf-8
import datetime
from .task import Task
class Storage:
def __init__(self):
self.agents={} # 此處用於存儲用戶信息
self.tasks={} # 此處用於存儲作業信息
def reg_hb(self,agent_id,info): # id 及就是客戶端的id ,info 及就是host和ip地址
self.agents[agent_id] = {
'heaerbeat' : datetime.datetime.now(),
'info' :info,
'busy':self.agents.get(agent_id,{}).get('busy',False)
}
# busy 讀不到置False,讀到了不變
def add_task(self,task:dict): # 此處用於從客戶端獲取相關的數據
t=Task(**task) # 此處進行參數解構
self.tasks[t.id]=t
return t.id # 此處用於獲取處理id
在master/handler.py 中處理用於webservr調用相關配置
#!/usr/bin/poython3.6
#conding:utf-8
from utils import getlogger
from .storage import Storage
import uuid
log=getlogger('handler')
class Handler(object):
def __init__(self):
self.store=Storage()
def send(self,msg): # 定義一個可調用的基礎函數,此處的msg及就是對應的函數
log.info('客戶端agent發送消息爲:{}'.format(msg))
try:
if msg['type'] in {'hearbeat','register'}:
payload=msg['payload']
info={'hostname' :payload['hostname'],'ip' :payload['ip']}
self.store.reg_hb(payload['id'],info)
log.info("客戶端數據列表爲:{}".format(self.store.agents)) # 客戶端的列表
return "agent信息爲: {}".format(msg)
except Exception as e:
log.error("註冊客戶端信息錯誤爲:{}".format(e))
return "Bad Request...."
def add_task(self,task): # 此處用於在webserver 端創建的agent調用方法返回結果
task['task_id']=uuid.uuid4().hex # 用於生成相關的任務id
return self.store.add_task(task) # 此處用於調用相關配置
def get_agents(self):
return self.store.get_agents()
6 task 任務分派
1 任務分派方式
任務在Storage中存儲,一旦有了任務,需要將任務分派到指定節點執行,交給這些節點上的agent
不過,目前使用zerorpc,master是被動的接受agent端的數據並進行相關的響應操作,所以可以考慮使用一種agent端主動拉取數據的機制,提供一個接口,讓agent訪問,如果agent處於空閒狀態,則就主動拉取任務,有任務就領走。
當agent少的時候,master推送任務到agent端,或者agent端主動拉取任務都是可以的,但是如果考慮到agent多的時候,或許使用agent拉模式是一個更好的選擇。本次採用agent拉取模式實現,所以master就不需要設計調度器了
2 客戶端配置狀態參數
agent/state.py
#!/usr/bin/poython3.6
#conding:utf-8
WAITING='WAITING'
RUNNING='RUNNING'
SUCCEED='SUCCEED'
FAILED='FAILED'
3 客戶端添加消息類型result
用於返回至server端,用於最後返回至web瀏覽器端
#!/usr/bin/poython3.6
#conding:utf-8
import socket
import uuid
import netifaces
import ipaddress
import os
class Messgae:
def __init__(self,myidpath):
if os.path.exists(myidpath): # 如果存在
with open(myidpath) as f:
self.id=f.readline().strip()
else:
self.id=uuid.uuid4().hex
with open(myidpath,'w') as f:
f.write(self.id)
def get_ipaddress(self):
address=[]
for p in netifaces.interfaces(): # 獲取網口列表
n=netifaces.ifaddresses(p) # 獲取字典
if n.get(2): # 查看是否存在ipv4地址
for ip in n[2]: # 此處獲取對應列表的值
if ip['addr']: # 查看ip地址是否存在
ip=ipaddress.ip_address(ip['addr'])
if ip.is_reserved or ip.is_multicast or ip.is_link_local or ip.is_loopback:
continue
address.append(str(ip))
return address
def hearbeat(self):
return {
"type" :"hearbeat",
"payload" :{
"ip" : self.get_ipaddress(),
"hostname" : socket.gethostname(),
"id" : self.id
}
}
def reg(self):
return {
"type" :"register",
"payload" :{
"ip" : self.get_ipaddress(),
"hostname" : socket.gethostname(),
"id" : self.id
}
}
def result(self,task_id,code,output): # 返回數據至web端,處理相關數據執行結果的返回
return {
"type" :"result",
"payload" :{
"id" : task_id, # 此處用於定義task_id 及任務id
"agent_id" :self.id, # 此處用於獲取客戶端id
"code" : code, # 此處用於對執行結果狀態進行保存
"output" : output #此處用於對執行結果的輸出信息進行保存,並進行相關配置
}
}
4 agent/cm.py模塊
用於處理配置拉取相關事宜
#!/usr/bin/poython3.6
#conding:utf-8
import zerorpc #添加模塊
import threading # 用於處理中斷相關
from .msg import Messgae # 獲取消息
from .state import *
from .config import CONN_URL
from .executor import Executor
from utils import getlogger
class Conn_Manager:
def __init__(self,timeout=3):
self.timeout=timeout
self.client=zerorpc.Client()
self.event=threading.Event()
self.message=Messgae('/var/log/mschedule/uuid') # 此處用於初始化消息
self.log=getlogger('agent') # 此處填寫相關的log日誌名稱
self.state=WAITING
self.exec=Executor()
def start(self):
try:
self.event.clear()
self.client.connect(CONN_URL) # 鏈接處理
self.log.info('註冊消息發送 {}'.format(self.client.send(self.message.reg()))) # 發送心跳信息
self.client.send(self.message.reg()) #處理註冊消息
while not self.event.wait(self.timeout): # 等待的時間
self.log.info('心跳消息發送 {}'.format(self.client.send(self.message.hearbeat()))) # 發送心跳信息
task=self.client.get_task(self.message.id) # 此處返回三個參數,1 爲taskid,二是script ,三是timeout
if task:
code,output=self.exec.run(task[1],task[2])
self.client.send(self.message.result(task[0],code,output))
else:
return "目前無消息"
except Exception as e:
self.event.set()
raise e # 此處是拋出異常到上一級
def shutdown(self):
self.log.info("關閉操作")
self.client.close()
self.event.set()
4 服務端相關task獲取配置
master/storage.py 用於配置獲取agent_id和task相關信息
#!/usr/bin/poython3.6
#conding:utf-8
import datetime
from .task import Task
from .state import *
class Storage:
def __init__(self):
self.agents={} # 此處用於存儲用戶信息
self.tasks={} # 此處用於存儲作業信息
def reg_hb(self,agent_id,info): # id 及就是客戶端的id ,info 及就是host和ip地址
self.agents[agent_id] = {
'heaerbeat' : datetime.datetime.now(),
'info' :info,
'busy':self.agents.get(agent_id,{}).get('busy',False)
}
# busy 讀不到置False,讀到了不變
def get_agents(self):
return self.agents
def add_task(self,task:dict): # 此處用於從客戶端獲取相關的數據
t=Task(**task) # 此處進行參數解構
self.tasks[t.id]=t
return t.id # 此處用於獲取處理id
@property
def itme_task(self):
yield from (task for task in self.tasks.values()) # 此處返回task
def get_task(self,agent_id):
return [task.id,task.script,task.timeout]
master/handler.py 配置如下
#!/usr/bin/poython3.6
#conding:utf-8
from utils import getlogger
from .storage import Storage
import uuid
log=getlogger('handler')
class Handler(object):
def __init__(self):
self.store=Storage()
def send(self,msg): # 定義一個可調用的基礎函數,此處的msg及就是對應的函數
log.info('客戶端agent發送消息爲:{}'.format(msg))
try:
if msg['type'] in {'hearbeat','register'}:
payload=msg['payload']
info={'hostname' :payload['hostname'],'ip' :payload['ip']}
self.store.reg_hb(payload['id'],info)
log.info("客戶端數據列表爲:{}".format(self.store.agents)) # 客戶端的列表
return "agent信息爲: {}".format(msg)
except Exception as e:
log.error("註冊客戶端信息錯誤爲:{}".format(e))
return "Bad Request...."
def add_task(self,task): # 此處用於在webserver 端創建的agent調用方法返回結果
task['task_id']=uuid.uuid4().hex # 用於生成相關的任務id
return self.store.add_task(task) # 此處用於調用相關配置
def get_agents(self):
return self.store.get_agents()
def get_task(self,agent_id):
return self.store.get_task(agent_id)
5 處理服務端接受result 消息處理機制
master/handler.py中配置
#!/usr/bin/poython3.6
#conding:utf-8
from utils import getlogger
from .storage import Storage
import uuid
log=getlogger('handler')
class Handler(object):
def __init__(self):
self.store=Storage()
def send(self,msg): # 定義一個可調用的基礎函數,此處的msg及就是對應的函數
log.info('客戶端agent發送消息爲:{}'.format(msg))
try:
if msg['type'] in {'hearbeat','register'}:
payload=msg['payload']
info={'hostname' :payload['hostname'],'ip' :payload['ip']}
self.store.reg_hb(payload['id'],info)
log.info("客戶端數據列表爲:{}".format(self.store.agents)) # 客戶端的列表
return "agent信息爲: {}".format(msg)
elif msg['type']=="result": # 此處用於處理相關返回信息
self.store.result(msg['payload']) # 調用對應方法
except Exception as e:
log.error("註冊客戶端信息錯誤爲:{}".format(e))
return "Bad Request...."
def add_task(self,task): # 此處用於在webserver 端創建的agent調用方法返回結果
task['task_id']=uuid.uuid4().hex # 用於生成相關的任務id
return self.store.add_task(task) # 此處用於調用相關配置
def get_agents(self):
return self.store.get_agents()
def get_task(self,agent_id):
return self.store.get_task(agent_id)
def get_result(self,task_id): # 此處返回對應的值
return self.store.get_result(task_id)
master/stroage.py端配置
#!/usr/bin/poython3.6
#conding:utf-8
import datetime
from .task import Task
from .state import *
class Storage:
def __init__(self):
self.agents={} # 此處用於存儲用戶信息
self.tasks={} # 此處用於存儲作業信息
self.result={} # 用於存儲agent端返回的結果
def reg_hb(self,agent_id,info): # id 及就是客戶端的id ,info 及就是host和ip地址
self.agents[agent_id] = {
'heaerbeat' : datetime.datetime.now().timestamp(),
'info' :info,
'busy':self.agents.get(agent_id,{}).get('busy',False)
}
# busy 讀不到置False,讀到了不變
def get_agents(self):
return self.agents
def add_task(self,task:dict): # 此處用於從客戶端獲取相關的數據
t=Task(**task) # 此處進行參數解構
self.tasks[t.id]=t
return t.id # 此處用於獲取處理id
@property
def itme_task(self):
yield from (task for task in self.tasks.values()) # 此處返回task
def get_task(self,agent_id):
for task in self.itme_task:
if agent_id in task.targets: # 此處用於判斷當前節點接入任務情況
return [task.id,task.script,task.timeout]
def add_result(self,payload:dict):
self.result[payload['id']]=payload # 此處以task_id 爲鍵,以payload爲值進行處理
def get_result(self,task_id:dict):
return self.result.get(task_id['task_id']) # task_id,獲取對應的payload值
五 web端配置和處理
1 概述
用戶通過WEB(HTTP)提交新的任務,任務json信息有:
1 任務腳本script,base64編碼
2 超時時間timeout
3 並行度 parallel
4 失敗率 fail_rate
5 失敗次數 fail_count
6 targets 是跑在agent上的agent_id 列表,可以讓用戶看到一個列表,通過列表的勾選來完成相關的操作
2 代碼實現
根目錄創建appwebserver.py配置
1 獲取agent相關列表
#!/usr/bin/poython3.6
#conding:utf-8
import zerorpc
from aiohttp import request,web_response,web,log
CONN_URL="tcp://127.0.0.1:9000"
client=zerorpc.Client()
client.connect(CONN_URL)
async def targetshandler(request:web.Request):
txt=client.get_agents() #通過zerorpc調用master端接口
return web.json_response(txt) # 返回json端數據
app=web.Application()
app.router.add_get('/task/targets',targetshandler) # 使用get方法進行處理
2 提交任務端配置
1 客戶端數據如下
{
"script" : "echo hello",
"timeout" :20,
"targets" :[]
}
2 添加提交數據接口
async def taskhandler(request:web.Request):
j = await request.json() # 獲取post 提交的數據,用於task任務數據生成
txt=client.add_task(j)
return web.Response(text=txt,status=201)
app.router.add_post('/task',taskhandler)
3 添加獲取執行結果配置
async def taskresult(request:web.Request):
j = await request.json()
txt =client.get_result(j)
return web.json_response(txt)
app.router.add_post('/result',taskresult)
4 整體代碼如下
#!/usr/bin/poython3.6
#conding:utf-8
import zerorpc
from aiohttp import request,web_response,web,log
CONN_URL="tcp://127.0.0.1:9000"
client=zerorpc.Client()
client.connect(CONN_URL)
async def targetshandler(request:web.Request):
txt=client.get_agents() #通過zerorpc調用master端接口
return web.json_response(txt) # 返回json端數據
app=web.Application()
app.router.add_get('/task/targets',targetshandler) # 使用get方法進行處理
async def taskhandler(request:web.Request):
j = await request.json()
txt=client.add_task(j)
return web.Response(text=txt,status=201)
app.router.add_post('/task',taskhandler)
async def taskresult(request:web.Request):
j = await request.json()
txt =client.get_result(j)
return web.json_response(txt)
app.router.add_post('/result',taskresult)
if __name__ == "__main__":
web.run_app(app,host='0.0.0.0',port=80)
3 測試結果如下
六 處理數據和節點狀態
1 狀態管理類型
1 節點狀態
當節點在進行相關事件調度處理時,其狀態應該是RUNNING狀態,當處理完成後,其狀態應該恢復稱爲WAITING狀態。
2 task 任務狀態
噹噹前agent下的所有該任務都執行完成時的狀態,此處設計較爲簡單,只是全部執行就將其狀態置位成功,否則爲RUNNING狀態或者WAITING,當有一個agent領取任務時,其狀態將被置爲RUNNING。
3 task中對應的agent的狀態
及就是當前節點執行當前任務的狀態,此狀態保存在task中的targets字典中,用於對其客戶端執行結果進行判斷而獲取其對應狀態。
2 客戶端調整代碼
主要是cm.py調整如下
#!/usr/bin/poython3.6
#conding:utf-8
import zerorpc #添加模塊
import threading # 用於處理中斷相關
from .msg import Messgae # 獲取消息
from .state import *
from .config import CONN_URL
from .executor import Executor
from utils import getlogger
class Conn_Manager:
def __init__(self,timeout=3):
self.timeout=timeout
self.client=zerorpc.Client()
self.event=threading.Event()
self.message=Messgae('/var/log/mschedule/uuid') # 此處用於初始化消息
self.log=getlogger('agent') # 此處填寫相關的log日誌名稱
self.state=WAITING
self.exec=Executor()
def start(self):
try:
self.event.clear()
self.client.connect(CONN_URL) # 鏈接處理
self.log.info('註冊消息發送 {}'.format(self.client.send(self.message.reg()))) # 發送心跳信息
self.client.send(self.message.reg()) #處理註冊消息
while not self.event.wait(self.timeout): # 等待的時間
self.log.info('心跳消息發送 {}'.format(self.client.send(self.message.hearbeat()))) # 發送心跳信息
if self.state == WAITING: # 如果此處是空閒狀態,則進行領任務處理
print('獲取任務task')
task = self.client.get_task(self.message.id) # 此處返回三個參數,1 爲taskid,二是script ,三是timeout
if task: # 領取成功,則進行執行相關任務.並上傳至服務器端其狀態
self.state = RUNNING # 此處任務成功的情況
code,output=self.exec.run(task[1],task[2])
self.client.send(self.message.result(task[0], code, output))
self.state=WAITING #狀態更新爲當前正常狀態
else:
return "目前無消息"
except Exception as e:
self.event.set()
raise e # 此處是拋出異常到上一級
def shutdown(self):
self.log.info("關閉操作")
self.client.close()
self.event.set()
3 master端代碼調整
master/storage.py
#!/usr/bin/poython3.6
#conding:utf-8
import datetime
from .task import Task
from .state import *
from utils import getlogger
log=getlogger('storage')
class Storage:
def __init__(self):
self.agents={} # 此處用於存儲用戶信息
self.tasks={} # 此處用於存儲作業信息
self.result={} # 用於存儲agent端返回的結果
self.task_state=0 # 用於處理當所有agent狀態都修改爲成功或失敗時將task的狀態也進行相關的修改
def reg_hb(self,agent_id,info): # id 及就是客戶端的id ,info 及就是host和ip地址
self.agents[agent_id] = {
'heaerbeat' : datetime.datetime.now().timestamp(),
'info' :info,
'busy':self.agents.get(agent_id,{}).get('busy',False)
}
# busy 讀不到置False,讀到了不變
def get_agents(self):
return self.agents
def add_task(self,task:dict): # 此處用於從客戶端獲取相關的數據
t=Task(**task) # 此處進行參數解構
self.tasks[t.id]=t
return t.id # 此處用於獲取處理id
@property
def itme_task(self):
yield from (task for task in self.tasks.values() if task.state in {WAITING,RUNNING}) # 此處返回task,當其中有成功或者失敗時,則不用進行相關的操作處理
#當爲WAITING或者RUNNING 時,則進行相關的操作,其他情況則不進行相關操作
def get_task(self,agent_id):
for task in self.itme_task:
if agent_id in task.targets: # 此處用於判斷當前節點接入任務情況
if task.state==WAITING:
task.state=RUNNING #當前消息的狀態
task.targets[agent_id]['state']=RUNNING # 此處是指此消息中的agent是否執行的狀態的處理,若獲取了,則此處的狀態爲RUNNING
return [task.id,task.script,task.timeout]
def add_result(self,payload:dict):
for task in self.itme_task:
if payload['code']==0:
task.targets[payload['agent_id']]['state']=SUCCEED # 此處是指對此消息進行處理,若code=0,則表示客戶端執行成功,若爲1,則表示失敗
self.task_state+=1
else:
task.targets[payload['agent_id']]['state']= FAILED#
self.task_state+=1
if self.task_state==task.target_count:
task.state=SUCCEED
self.task_state=0
payload['agent_state']=task.targets[payload['agent_id']]['state']
log.info("當前消息內容爲:{}".format(self.result))
self.result[payload['id']]=payload # 此處以task_id 爲鍵,以payload爲值進行處理
def get_result(self,task_id:dict):
task_id=task_id['task_id']
return self.result.get(task_id) # task_id,獲取對應的payload值
4 webserver端代碼調整如下
webappserver.py
#!/usr/bin/poython3.6
#conding:utf-8
import zerorpc
from aiohttp import request,web_response,web,log
CONN_URL="tcp://127.0.0.1:9000"
client=zerorpc.Client()
client.connect(CONN_URL)
async def targetshandler(request:web.Request):
txt=client.get_agents() #通過zerorpc調用master端接口
return web.json_response(txt) # 返回json端數據
app=web.Application()
app.router.add_get('/task/targets',targetshandler) # 使用get方法進行處理
async def taskhandler(request:web.Request):
j = await request.json()
txt=client.add_task(j)
return web.Response(text=txt,status=201)
app.router.add_post('/task',taskhandler)
async def taskresult(request:web.Request):
j = await request.json()
txt =client.get_result(j)
if txt['code'] !=0:
txt['output']='參數不正確,請重新輸入'
return web.json_response(txt)
app.router.add_post('/result',taskresult)
if __name__ == "__main__":
web.run_app(app,host='0.0.0.0',port=80)