├── .gitignore ├── README.md ├── cfg.example.json ├── control ├── g ├── cfg.go ├── g.go └── parser.go ├── http ├── common.go ├── heartbeat.go ├── heartbeat_test.go ├── http.go ├── proc.go └── tarball.go ├── main.go └── store ├── agents.go └── cleaner.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | *.sw[op] 25 | /cfg.json 26 | /var 27 | /log 28 | /tmp 29 | /.idea 30 | *.iml 31 | *.log 32 | /ops-meta* 33 | /meta* 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # meta 2 | 3 | 接收ops-updater汇报上来的agent real state,返回最新的agent desired state 4 | 5 | ## 设计理念 6 | 7 | - 对于一个公司而言,agent并不多,也就有个监控agent、部署agent、naming agent,所以ops-meta直接采用配置文件而不是数据库之类的大型存储来存放agent信息 8 | - 公司级别agent升级慢一点没关系,比如一晚上升级完问题都不大,所以ops-updater与ops-meta的通信周期默认是5min,比较长。如果做成长连接,周期调小,是否就可以不光用来部署agent,也可以部署一些业务程序?不要这么做!部署其他业务组件是部署agent的责任,ops-updater做的事情少才不容易出错。ops-updater推荐在装机的时候直接安装好,功能少基本不升级。 9 | - 配置文件中针对各个agent有个default配置,有个others配置,这个others配置是为了解决小流量问题,对于某些前缀的机器可以采用与default不同的配置,也就间接解决了小流量测试问题 10 | - ops-updater会汇报自己管理的各个agent的状态、版本号,这个信息直接存放在ops-meta模块的内存中,因为数据量真没多少,100w机器,3个agent…… 11 | 12 | ## 使用方法 13 | 14 | - 1. 把要升级的agent打好tarball,交给http server 15 | - 2. agent命名规范是`-.tar.gz`,md5生成方式和命名:`md5sum -.tar.gz > -.tar.gz.md5`,比如:falcon-agent,全名:falcon-agent-1.0.0.tar.gz 16 | - 3. 修改ops-meta的配置文件,agent太重要了,最好有个admin专门来审核、上线 17 | - 4. 修改完配置之后无需重启,`curl 127.0.0.1:2000/config/reload`即可自动reload配置,如果成功,会把配置信息打印出来 18 | - 5. `curl 127.0.0.1:2000/status/json/falcon-agent`列出falcon-agent在各个机器上的部署情况,返回json格式 19 | - 6. `curl 127.0.0.1:2000/status/text/falcon-agent`列出falcon-agent在各个机器上的部署情况,返回文本格式 20 | 21 | agent tarball最终下载地址是:`{$tarball}/{$name}-{$version}.tar.gz`,为啥不在tarball这里配置成全路径呢?为了规范!就是这么横! 22 | 23 | ## 注意 24 | 25 | - 虽然ops-meta提供了http服务,可以直接用来提供tarball下载,但是不推荐这样用,最好单独再搭建一个服务(比如nginx,如果觉得麻烦再搭一个ops-meta专门用于下载都可以)专门用于文件下载,这样ops-meta做的事情少,稳定。 26 | -------------------------------------------------------------------------------- /cfg.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug": true, 3 | "tarballDir": "./tarball", 4 | "http": { 5 | "enabled": true, 6 | "listen": "0.0.0.0:2000" 7 | }, 8 | "agents": [ 9 | { 10 | "default": { 11 | "name": "falcon-agent", 12 | "version": "0.0.1", 13 | "tarball": "http://11.11.11.11:8888/falcon", 14 | "md5": "", 15 | "cmd": "start" 16 | }, 17 | "others": [ 18 | { 19 | "prefix": "lg-falcon", 20 | "version": "1.0.0", 21 | "tarball": "", 22 | "md5": "", 23 | "cmd": "" 24 | }, 25 | { 26 | "prefix": "lg-dinp", 27 | "version": "1.0.1", 28 | "tarball": "", 29 | "md5": "", 30 | "cmd": "" 31 | } 32 | ] 33 | }, 34 | { 35 | "default": { 36 | "name": "dinp-agent", 37 | "version": "0.0.1", 38 | "tarball": "http://11.11.11.11:8888/dinp", 39 | "md5": "", 40 | "cmd": "start" 41 | } 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WORKSPACE=$(cd $(dirname $0)/; pwd) 4 | cd $WORKSPACE 5 | 6 | mkdir -p var 7 | 8 | module=meta-https 9 | app=ops-$module 10 | conf=cfg.json 11 | pidfile=var/app.pid 12 | logfile=var/app.log 13 | 14 | function check_pid() { 15 | if [ -f $pidfile ];then 16 | pid=`cat $pidfile` 17 | if [ -n $pid ]; then 18 | running=`ps -p $pid|grep -v "PID TTY" |wc -l` 19 | return $running 20 | fi 21 | fi 22 | return 0 23 | } 24 | 25 | function start() { 26 | check_pid 27 | running=$? 28 | if [ $running -gt 0 ];then 29 | echo -n "$app now is running already, pid=" 30 | cat $pidfile 31 | return 1 32 | fi 33 | 34 | nohup ./$app -c $conf &> $logfile & 35 | echo $! > $pidfile 36 | echo "$app started..., pid=$!" 37 | } 38 | 39 | function stop() { 40 | pid=`cat $pidfile` 41 | kill $pid 42 | echo "$app stoped..." 43 | } 44 | 45 | function restart() { 46 | stop 47 | sleep 1 48 | start 49 | } 50 | 51 | function status() { 52 | check_pid 53 | running=$? 54 | if [ $running -gt 0 ];then 55 | echo "started" 56 | else 57 | echo "stoped" 58 | fi 59 | } 60 | 61 | function tailf() { 62 | tail -f $logfile 63 | } 64 | 65 | function build() { 66 | go build -a 67 | if [ $? -ne 0 ]; then 68 | exit $? 69 | fi 70 | mv $module $app 71 | ./$app -v 72 | } 73 | 74 | function pack() { 75 | build 76 | version=`./$app -v` 77 | tar zcvf $app-$version.tar.gz control cfg.example.json $app 78 | } 79 | 80 | function packbin() { 81 | build 82 | version=`./$app -v` 83 | tar zcvf $app-bin-$version.tar.gz $app 84 | } 85 | 86 | function help() { 87 | echo "$0 pid|reload|build|pack|packbin|start|stop|restart|status|tail" 88 | } 89 | 90 | function pid() { 91 | cat $pidfile 92 | } 93 | 94 | function reload() { 95 | curl -s 127.0.0.1:2000/config/reload | python -m json.tool 96 | } 97 | 98 | if [ "$1" == "" ]; then 99 | help 100 | elif [ "$1" == "stop" ];then 101 | stop 102 | elif [ "$1" == "start" ];then 103 | start 104 | elif [ "$1" == "restart" ];then 105 | restart 106 | elif [ "$1" == "status" ];then 107 | status 108 | elif [ "$1" == "tail" ];then 109 | tailf 110 | elif [ "$1" == "build" ];then 111 | build 112 | elif [ "$1" == "pack" ];then 113 | pack 114 | elif [ "$1" == "packbin" ];then 115 | packbin 116 | elif [ "$1" == "pid" ];then 117 | pid 118 | elif [ "$1" == "reload" ];then 119 | reload 120 | else 121 | help 122 | fi 123 | -------------------------------------------------------------------------------- /g/cfg.go: -------------------------------------------------------------------------------- 1 | package g 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/toolkits/file" 7 | "log" 8 | "sync" 9 | ) 10 | 11 | type HttpConfig struct { 12 | Enabled bool `json:"enabled"` 13 | Listen string `json:"listen"` 14 | } 15 | 16 | type AgentDefaultConfig struct { 17 | Name string `json:"name"` 18 | Version string `json:"version"` 19 | Tarball string `json:"tarball"` 20 | Md5 string `json:"md5"` 21 | Cmd string `json:"cmd"` 22 | } 23 | 24 | type AgentOtherConfig struct { 25 | Prefix string `json:"prefix"` 26 | Version string `json:"version"` 27 | Tarball string `json:"tarball"` 28 | Md5 string `json:"md5"` 29 | Cmd string `json:"cmd"` 30 | } 31 | 32 | type InheritConfig struct { 33 | Default *AgentDefaultConfig `json:"default"` 34 | Others []*AgentOtherConfig `json:"others"` 35 | } 36 | 37 | type GlobalConfig struct { 38 | Debug bool `json:"debug"` 39 | TarballDir string `json:"tarballDir"` 40 | Http *HttpConfig `json:"http"` 41 | Agents []*InheritConfig `json:"agents"` 42 | } 43 | 44 | var ( 45 | ConfigFile string 46 | config *GlobalConfig 47 | configLock = new(sync.RWMutex) 48 | ) 49 | 50 | func Config() *GlobalConfig { 51 | configLock.RLock() 52 | defer configLock.RUnlock() 53 | return config 54 | } 55 | 56 | func ParseConfig(cfg string) error { 57 | if cfg == "" { 58 | return fmt.Errorf("use -c to specify configuration file") 59 | } 60 | 61 | if !file.IsExist(cfg) { 62 | return fmt.Errorf("config file %s is nonexistent", cfg) 63 | } 64 | 65 | ConfigFile = cfg 66 | 67 | configContent, err := file.ToTrimString(cfg) 68 | if err != nil { 69 | return fmt.Errorf("read config file %s fail %s", cfg, err) 70 | } 71 | 72 | var c GlobalConfig 73 | err = json.Unmarshal([]byte(configContent), &c) 74 | if err != nil { 75 | return fmt.Errorf("parse config file %s fail %s", cfg, err) 76 | } 77 | 78 | configLock.Lock() 79 | defer configLock.Unlock() 80 | 81 | config = &c 82 | 83 | log.Println("read config file:", cfg, "successfully") 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /g/g.go: -------------------------------------------------------------------------------- 1 | package g 2 | 3 | import ( 4 | "log" 5 | "runtime" 6 | ) 7 | 8 | const ( 9 | VERSION = "0.0.2" 10 | ) 11 | 12 | func init() { 13 | runtime.GOMAXPROCS(runtime.NumCPU()) 14 | log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) 15 | } 16 | -------------------------------------------------------------------------------- /g/parser.go: -------------------------------------------------------------------------------- 1 | package g 2 | 3 | import ( 4 | "github.com/Cepave/ops-common/model" 5 | "log" 6 | "strings" 7 | ) 8 | 9 | func DesiredAgents(hostname string) (desiredAgents []*model.DesiredAgent) { 10 | config := Config() 11 | for _, inheritConfig := range config.Agents { 12 | defaultConfig := inheritConfig.Default 13 | if defaultConfig == nil { 14 | log.Println("default configuration is missed") 15 | continue 16 | } 17 | 18 | desiredAgent := &model.DesiredAgent{ 19 | Name: defaultConfig.Name, 20 | Version: defaultConfig.Version, 21 | Tarball: defaultConfig.Tarball, 22 | Md5: defaultConfig.Md5, 23 | Cmd: defaultConfig.Cmd, 24 | } 25 | 26 | others := inheritConfig.Others 27 | if others != nil && len(others) > 0 { 28 | for _, otherConfig := range inheritConfig.Others { 29 | if otherConfig == nil { 30 | continue 31 | } 32 | 33 | if !strings.HasPrefix(hostname, otherConfig.Prefix) { 34 | continue 35 | } 36 | 37 | if otherConfig.Version != "" { 38 | desiredAgent.Version = otherConfig.Version 39 | } 40 | 41 | if otherConfig.Tarball != "" { 42 | desiredAgent.Tarball = otherConfig.Tarball 43 | } 44 | 45 | if otherConfig.Md5 != "" { 46 | desiredAgent.Md5 = otherConfig.Md5 47 | } 48 | 49 | if otherConfig.Cmd != "" { 50 | desiredAgent.Cmd = otherConfig.Cmd 51 | } 52 | } 53 | } 54 | 55 | desiredAgents = append(desiredAgents, desiredAgent) 56 | } 57 | 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /http/common.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/Cepave/ops-meta/g" 5 | "github.com/toolkits/file" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | func configCommonRoutes() { 11 | http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { 12 | w.Write([]byte("ok")) 13 | }) 14 | 15 | http.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { 16 | w.Write([]byte(g.VERSION)) 17 | }) 18 | 19 | http.HandleFunc("/workdir", func(w http.ResponseWriter, r *http.Request) { 20 | RenderDataJson(w, file.SelfDir()) 21 | }) 22 | 23 | http.HandleFunc("/config/reload", func(w http.ResponseWriter, r *http.Request) { 24 | if strings.HasPrefix(r.RemoteAddr, "127.0.0.1") { 25 | err := g.ParseConfig(g.ConfigFile) 26 | AutoRender(w, g.Config(), err) 27 | } else { 28 | w.Write([]byte("no privilege")) 29 | } 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /http/heartbeat.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/Cepave/ops-common/model" 6 | "github.com/Cepave/ops-meta/g" 7 | "github.com/Cepave/ops-meta/store" 8 | "log" 9 | "net/http" 10 | ) 11 | 12 | func configHeartbeatRoutes() { 13 | // post 14 | http.HandleFunc("/heartbeat", func(w http.ResponseWriter, r *http.Request) { 15 | if r.ContentLength == 0 { 16 | http.Error(w, "body is blank", http.StatusBadRequest) 17 | return 18 | } 19 | 20 | var req model.HeartbeatRequest 21 | decoder := json.NewDecoder(r.Body) 22 | err := decoder.Decode(&req) 23 | if err != nil { 24 | http.Error(w, "body format error", http.StatusBadRequest) 25 | return 26 | } 27 | 28 | if req.Hostname == "" { 29 | http.Error(w, "hostname is blank", http.StatusBadRequest) 30 | return 31 | } 32 | 33 | if g.Config().Debug { 34 | log.Println("Heartbeat Request=====>>>>") 35 | log.Println(req) 36 | } 37 | 38 | store.ParseHeartbeatRequest(&req) 39 | 40 | resp := model.HeartbeatResponse{ 41 | ErrorMessage: "", 42 | DesiredAgents: g.DesiredAgents(req.Hostname), 43 | } 44 | 45 | // TODO: This is a workaround to ensure that one updater is resposible for only one agent, 46 | // so NQM agent(resp.DesiredAgents[1]) would be stopped if the request comes from owl-agent-updater. 47 | if len(resp.DesiredAgents) > 1 && len(req.RealAgents) != 1 { 48 | resp.DesiredAgents[1].Cmd = "stop" 49 | } 50 | 51 | if g.Config().Debug { 52 | log.Println("<<<<=====Heartbeat Response") 53 | log.Println(resp) 54 | } 55 | 56 | RenderJson(w, resp) 57 | 58 | }) 59 | 60 | } 61 | -------------------------------------------------------------------------------- /http/heartbeat_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/Cepave/ops-common/model" 7 | "github.com/Cepave/ops-meta/g" 8 | "github.com/toolkits/net/httplib" 9 | "net/http" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func sendHeartbeatReq(t *testing.T) { 15 | req := model.HeartbeatRequest{Hostname: "cnc-bj-123-123-123-123"} 16 | realAgents := []*model.RealAgent{} 17 | realAgent := &model.RealAgent{ 18 | Name: "installed-agent1", 19 | Version: "5.1.4", 20 | Status: "started", 21 | Timestamp: 1465874871, 22 | } 23 | realAgents = append(realAgents, realAgent) 24 | realAgent = &model.RealAgent{ 25 | Name: "installed-agent2", 26 | Version: "0.9.1", 27 | Status: "stoped", 28 | Timestamp: 1465874871, 29 | } 30 | realAgents = append(realAgents, realAgent) 31 | req.RealAgents = realAgents 32 | bs, err := json.Marshal(req) 33 | url := fmt.Sprintf("http://localhost:%s/heartbeat", "9002") 34 | httpRequest := httplib.Post(url).SetTimeout(time.Second*10, time.Minute) 35 | httpRequest.Body(bs) 36 | _, err = httpRequest.Bytes() 37 | if err != nil { 38 | fmt.Println(err) 39 | } 40 | } 41 | 42 | func start() { 43 | err := http.ListenAndServe(":9002", nil) 44 | if err != nil { 45 | fmt.Println("ListenAndServe failed: ", err) 46 | } else { 47 | fmt.Println("OK") 48 | } 49 | } 50 | 51 | func TestKordan(t *testing.T) { 52 | fmt.Println("test start") 53 | if err := g.ParseConfig("../cfg.example.json"); err != nil { 54 | fmt.Println(err) 55 | } 56 | go start() 57 | time.Sleep(500 * time.Millisecond) 58 | sendHeartbeatReq(t) 59 | } 60 | -------------------------------------------------------------------------------- /http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/Cepave/ops-meta/g" 6 | "log" 7 | "net/http" 8 | _ "net/http/pprof" 9 | ) 10 | 11 | type Dto struct { 12 | Msg string `json:"msg"` 13 | Data interface{} `json:"data"` 14 | } 15 | 16 | func init() { 17 | configCommonRoutes() 18 | configProcRoutes() 19 | configTarballRoutes() 20 | configHeartbeatRoutes() 21 | } 22 | 23 | func RenderJson(w http.ResponseWriter, v interface{}) { 24 | bs, err := json.Marshal(v) 25 | if err != nil { 26 | http.Error(w, err.Error(), http.StatusInternalServerError) 27 | return 28 | } 29 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 30 | w.Write(bs) 31 | } 32 | 33 | func RenderDataJson(w http.ResponseWriter, data interface{}) { 34 | RenderJson(w, Dto{Msg: "success", Data: data}) 35 | } 36 | 37 | func RenderMsgJson(w http.ResponseWriter, msg string) { 38 | RenderJson(w, map[string]string{"msg": msg}) 39 | } 40 | 41 | func AutoRender(w http.ResponseWriter, data interface{}, err error) { 42 | if err != nil { 43 | RenderMsgJson(w, err.Error()) 44 | return 45 | } 46 | RenderDataJson(w, data) 47 | } 48 | 49 | func Start() { 50 | if !g.Config().Http.Enabled { 51 | return 52 | } 53 | 54 | addr := g.Config().Http.Listen 55 | if addr == "" { 56 | return 57 | } 58 | //s := &http.Server{ 59 | // Addr: addr, 60 | // MaxHeaderBytes: 1 << 30, 61 | //} 62 | log.Println("https listening", addr) 63 | err := http.ListenAndServeTLS(addr, "cert.pem", "key.pem", nil) 64 | log.Fatalln(err) 65 | } 66 | -------------------------------------------------------------------------------- /http/proc.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Cepave/ops-meta/store" 6 | "net/http" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func configProcRoutes() { 12 | 13 | http.HandleFunc("/status/json/", func(w http.ResponseWriter, r *http.Request) { 14 | agentName := r.URL.Path[len("/status/json/"):] 15 | if agentName == "" { 16 | http.Error(w, "agent name is blank", http.StatusBadRequest) 17 | return 18 | } 19 | 20 | data := store.HostAgents.Status(agentName) 21 | RenderJson(w, data) 22 | }) 23 | 24 | http.HandleFunc("/status/text/", func(w http.ResponseWriter, r *http.Request) { 25 | agentName := r.URL.Path[len("/status/text/"):] 26 | if agentName == "" { 27 | http.Error(w, "agent name is blank", http.StatusBadRequest) 28 | return 29 | } 30 | 31 | data := store.HostAgents.Status(agentName) 32 | arr := make([]string, len(data)) 33 | i := 0 34 | for hostname, ra := range data { 35 | if ra != nil { 36 | arr[i] = fmt.Sprintf( 37 | "%s %s %s %v %s\n", 38 | hostname, 39 | ra.Version, 40 | ra.Status, 41 | ra.Timestamp, 42 | time.Unix(ra.Timestamp, 0).Format("2006-01-02 15:04:05"), 43 | ) 44 | } else { 45 | arr[i] = fmt.Sprintf("%s not found\n", hostname) 46 | } 47 | 48 | i++ 49 | } 50 | 51 | w.Write([]byte(strings.Join(arr, ""))) 52 | 53 | }) 54 | 55 | } 56 | -------------------------------------------------------------------------------- /http/tarball.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/base64" 5 | "github.com/Cepave/ops-meta/g" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | func configTarballRoutes() { 12 | 13 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 14 | auth := strings.SplitN(r.Header["Authorization"][0], " ", 2) 15 | 16 | if len(auth) != 2 || auth[0] != "Basic" { 17 | http.Error(w, "bad syntax", http.StatusBadRequest) 18 | return 19 | } 20 | 21 | payload, _ := base64.StdEncoding.DecodeString(auth[1]) 22 | pair := strings.SplitN(string(payload), ":", 2) 23 | 24 | if len(pair) != 2 || !Validate(pair[0], pair[1]) { 25 | http.Error(w, "authorization failed", http.StatusUnauthorized) 26 | return 27 | } 28 | 29 | http.FileServer(http.Dir(g.Config().TarballDir)).ServeHTTP(w, r) 30 | }) 31 | 32 | } 33 | 34 | func Validate(username, password string) bool { 35 | 36 | content, err := ioutil.ReadFile("./username") 37 | fUsername := strings.Trim(string(content), "\n") 38 | if err != nil { 39 | panic(err) 40 | } 41 | content, err = ioutil.ReadFile("./password") 42 | fPassword := strings.Trim(string(content), "\n") 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | if username == fUsername && password == fPassword { 48 | return true 49 | } 50 | return false 51 | } 52 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/Cepave/ops-meta/g" 7 | "github.com/Cepave/ops-meta/http" 8 | "github.com/Cepave/ops-meta/store" 9 | "log" 10 | "os" 11 | ) 12 | 13 | func main() { 14 | cfg := flag.String("c", "cfg.json", "configuration file") 15 | version := flag.Bool("v", false, "show version") 16 | flag.Parse() 17 | 18 | if *version { 19 | fmt.Println(g.VERSION) 20 | os.Exit(0) 21 | } 22 | 23 | if err := g.ParseConfig(*cfg); err != nil { 24 | log.Fatalln(err) 25 | } 26 | 27 | go http.Start() 28 | go store.CleanStaleHost() 29 | 30 | select {} 31 | } 32 | -------------------------------------------------------------------------------- /store/agents.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "github.com/Cepave/ops-common/model" 5 | "sync" 6 | ) 7 | 8 | type AgentsMap struct { 9 | sync.RWMutex 10 | M map[string]*model.RealAgent 11 | } 12 | 13 | func NewAgentsMap() *AgentsMap { 14 | return &AgentsMap{M: make(map[string]*model.RealAgent)} 15 | } 16 | 17 | func (this *AgentsMap) Get(agentName string) (*model.RealAgent, bool) { 18 | this.RLock() 19 | defer this.RUnlock() 20 | val, exists := this.M[agentName] 21 | return val, exists 22 | } 23 | 24 | func (this *AgentsMap) Len() int { 25 | this.RLock() 26 | defer this.RUnlock() 27 | return len(this.M) 28 | } 29 | 30 | func (this *AgentsMap) IsStale(before int64) bool { 31 | this.RLock() 32 | defer this.RUnlock() 33 | for _, ra := range this.M { 34 | if ra.Timestamp > before { 35 | return false 36 | } 37 | } 38 | return true 39 | } 40 | 41 | func (this *AgentsMap) Put(agentName string, realAgent *model.RealAgent) { 42 | this.Lock() 43 | defer this.Unlock() 44 | this.M[agentName] = realAgent 45 | } 46 | 47 | type HostAgentsMap struct { 48 | sync.RWMutex 49 | M map[string]*AgentsMap 50 | } 51 | 52 | func NewHostAgentsMap() *HostAgentsMap { 53 | return &HostAgentsMap{M: make(map[string]*AgentsMap)} 54 | } 55 | 56 | var HostAgents = NewHostAgentsMap() 57 | 58 | func (this *HostAgentsMap) Get(hostname string) (*AgentsMap, bool) { 59 | this.RLock() 60 | defer this.RUnlock() 61 | val, exists := this.M[hostname] 62 | return val, exists 63 | } 64 | 65 | func (this *HostAgentsMap) Put(hostname string, am *AgentsMap) { 66 | this.Lock() 67 | defer this.Unlock() 68 | this.M[hostname] = am 69 | } 70 | 71 | func (this *HostAgentsMap) Hostnames() []string { 72 | this.RLock() 73 | defer this.RUnlock() 74 | 75 | count := len(this.M) 76 | hostnames := make([]string, count) 77 | 78 | i := 0 79 | for hostname := range this.M { 80 | hostnames[i] = hostname 81 | i++ 82 | } 83 | 84 | return hostnames 85 | } 86 | 87 | func (this *HostAgentsMap) Delete(hostname string) { 88 | this.Lock() 89 | defer this.Unlock() 90 | delete(this.M, hostname) 91 | } 92 | 93 | func (this *HostAgentsMap) Status(agentName string) (ret map[string]*model.RealAgent) { 94 | ret = make(map[string]*model.RealAgent) 95 | this.RLock() 96 | defer this.RUnlock() 97 | for hostname, agents := range this.M { 98 | ra, exists := agents.Get(agentName) 99 | if !exists { 100 | ret[hostname] = nil 101 | } else { 102 | ret[hostname] = ra 103 | } 104 | } 105 | return 106 | } 107 | 108 | func ParseHeartbeatRequest(req *model.HeartbeatRequest) { 109 | if req.RealAgents == nil || len(req.RealAgents) == 0 { 110 | return 111 | } 112 | 113 | agentsMap, exists := HostAgents.Get(req.Hostname) 114 | 115 | if exists { 116 | for _, a := range req.RealAgents { 117 | agentsMap.Put(a.Name, a) 118 | } 119 | } else { 120 | am := NewAgentsMap() 121 | for _, a := range req.RealAgents { 122 | am.Put(a.Name, a) 123 | } 124 | HostAgents.Put(req.Hostname, am) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /store/cleaner.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | func CleanStaleHost() { 8 | d := time.Duration(24) * time.Hour 9 | for { 10 | time.Sleep(d) 11 | cleanStaleHost() 12 | } 13 | } 14 | 15 | func cleanStaleHost() { 16 | // three days ago 17 | before := time.Now().Unix() - 3600*24*3 18 | 19 | hostnames := HostAgents.Hostnames() 20 | count := len(hostnames) 21 | if count == 0 { 22 | return 23 | } 24 | 25 | for i := 0; i < count; i++ { 26 | agentsMap, exists := HostAgents.Get(hostnames[i]) 27 | if !exists { 28 | continue 29 | } 30 | 31 | if agentsMap == nil || agentsMap.Len() == 0 { 32 | HostAgents.Delete(hostnames[i]) 33 | } 34 | 35 | if agentsMap.IsStale(before) { 36 | HostAgents.Delete(hostnames[i]) 37 | } 38 | } 39 | } 40 | --------------------------------------------------------------------------------