From 33c84396041dd598a6208c0cddac1abf46c3c005 Mon Sep 17 00:00:00 2001 From: Henrik Hautakoski Date: Mon, 3 Feb 2020 13:24:34 +0100 Subject: [PATCH] Initial commit --- .gitignore | 1 + Makefile | 31 ++++++++++++ eosapi/functions.go | 44 ++++++++++++++++ eosapi/types.go | 11 ++++ haproxy/types.go | 15 ++++++ scripts/.gitignore | 2 + scripts/build_deb.sh | 41 +++++++++++++++ scripts/template.service | 20 ++++++++ server.go | 105 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 270 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 eosapi/functions.go create mode 100644 eosapi/types.go create mode 100644 haproxy/types.go create mode 100644 scripts/.gitignore create mode 100755 scripts/build_deb.sh create mode 100644 scripts/template.service create mode 100644 server.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..29ce7ea --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ + +GO = go +GOCCFLAGS = -v +GOLDFLAGS = +PREFIX = /usr/local + +PROGRAM_NAME=eos-api-healthcheck +SOURCES=server.go +DEPENDANCIES= github.com/firstrow/tcp_server \ + github.com/liamylian/jsontime/v2 \ + github.com/imroc/req + +all: build +build: build/$(PROGRAM_NAME) + +build/$(PROGRAM_NAME) : $(SOURCES) + $(GO) build -o $@ $(GOCCFLAGS) $(GOLDFLAGS) $< + +deps: + $(GO) get $(DEPENDANCIES) + +package_deb: build + export PACKAGE_NAME="$(PROGRAM_NAME)" \ + export PACKAGE_VERSION="0.1.0" \ + export PACKAGE_PREFIX=$(PREFIX:/%=%) \ + export PACKAGE_PROGRAM="build/$(PROGRAM_NAME)" \ + && ./scripts/build_deb.sh + +clean: + $(GO) clean + $(RM) -rf build/ diff --git a/eosapi/functions.go b/eosapi/functions.go new file mode 100644 index 0000000..ee9260a --- /dev/null +++ b/eosapi/functions.go @@ -0,0 +1,44 @@ + +package eosapi; + +import ( + "fmt" + "time" + "io/ioutil" + "github.com/imroc/req" + "github.com/liamylian/jsontime/v2" +) + +var json = v2.ConfigWithCustomTimeFormat; + +func init() { + + // EOS Api does not specify timezone in timestamps (they are always UTC tho). + v2.SetDefaultTimeFormat("2006-01-02T15:04:05", time.UTC); +} + +// GetInfo - Fetches get_info from API +// --------------------------------------------------------- +func GetInfo(host string, port int) (Info, error) { + + var info Info; + + // Format url. + url := fmt.Sprintf("http://%s:%d/v1/chain/get_info", host, port); + + // Go's net.http (that `req` uses) sends the port in the host header. + // nodeos api does not like that, so we need to provide our + // own Host header with just the host. + headers := req.Header{ + "Host": host, + } + + // Send HTTP Get request. + r, err := req.Get(url, headers) + resp := r.Response() + body, _ := ioutil.ReadAll(resp.Body); + + // Parse json + err = json.Unmarshal(body, &info); + return info, err; +} diff --git a/eosapi/types.go b/eosapi/types.go new file mode 100644 index 0000000..4b31dac --- /dev/null +++ b/eosapi/types.go @@ -0,0 +1,11 @@ + +package eosapi; + +import "time"; + +// get_info format (not all fields). +type Info struct { + ServerVersion string `json:"server_version"` + HeadBlockNum int64 `json:"head_block_num"` + HeadBlockTime time.Time `json:"head_block_time"` +} diff --git a/haproxy/types.go b/haproxy/types.go new file mode 100644 index 0000000..118aea5 --- /dev/null +++ b/haproxy/types.go @@ -0,0 +1,15 @@ + +package haproxy; + +// All supported health check values for HAproxy. +// See https://cbonte.github.io/haproxy-dconv/1.7/configuration.html#5.2-agent-check +type HealthCheckStatus string +const ( + HealthCheckUp = "up" + HealthCheckDown = "down" + HealthCheckMaint = "maint" + HealthCheckReady = "ready" + HealthCheckDrain = "drain" + HealthCheckFailed = "failed" + HealthCheckStopped = "Stopped" +) diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..a44cf89 --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1,2 @@ +pack/ +*.deb diff --git a/scripts/build_deb.sh b/scripts/build_deb.sh new file mode 100755 index 0000000..1ca0ecf --- /dev/null +++ b/scripts/build_deb.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +BASE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +PACKAGE_DESCRIPTION="HAproxy healthcheck program for EOS API." +PACKAGE_TMPDIR="pack" + +# Default to 1 if no release is set. +if [[ -z $RELEASE ]]; then + RELEASE="1" +fi + +PACKAGE_FULLNAME="${PACKAGE_NAME}_${PACKAGE_VERSION}-${RELEASE}_amd64" + +rm -fr ${BASE_DIR}/${PACKAGE_TMPDIR} + +# Create debian files. +mkdir -p ${BASE_DIR}/${PACKAGE_TMPDIR}/DEBIAN +echo "Package: ${PACKAGE_NAME} +Version: ${PACKAGE_VERSION}-${RELEASE} +Section: introspection +Priority: optional +Architecture: amd64 +Homepage: https://github.com/eosswedenorg/eos-api-healthcheck +Maintainer: Henrik Hautakoski +Description: ${PACKAGE_DESCRIPTION}" &> ${BASE_DIR}/${PACKAGE_TMPDIR}/DEBIAN/control + +cat ${BASE_DIR}/${PACKAGE_TMPDIR}/DEBIAN/control + +# Create service file +mkdir -p ${BASE_DIR}/${PACKAGE_TMPDIR}/etc/systemd/system +cat ${BASE_DIR}/template.service \ + | sed "s~{{ DESCRIPTION }}~${PACKAGE_DESCRIPTION}~" \ + | sed "s~{{ PROGRAM }}~/${PACKAGE_PREFIX}/bin/${PACKAGE_NAME}~" \ + > ${BASE_DIR}/${PACKAGE_TMPDIR}/etc/systemd/system/${PACKAGE_NAME}.service + +# Copy program +mkdir -p ${BASE_DIR}/${PACKAGE_TMPDIR}/${PACKAGE_PREFIX}/bin +cp ${BASE_DIR}/../${PACKAGE_PROGRAM} ${BASE_DIR}/${PACKAGE_TMPDIR}/${PACKAGE_PREFIX}/bin/${PACKAGE_NAME} + +fakeroot dpkg-deb --build ${BASE_DIR}/${PACKAGE_TMPDIR} ${BASE_DIR}/${PACKAGE_FULLNAME}.deb diff --git a/scripts/template.service b/scripts/template.service new file mode 100644 index 0000000..b06ab95 --- /dev/null +++ b/scripts/template.service @@ -0,0 +1,20 @@ +[Unit] +Description={{ DESCRIPTION }} +After=network.target + +[Service] +Type=simple +# Another Type: forking +#User=nanodano +#WorkingDirectory=/home/nanodano +ExecStart={{ PROGRAM }} +Restart=on-failure +# Other restart options: always, on-abort, etc + +# The install section is needed to use +# `systemctl enable` to start on boot +# For a user service that you want to enable +# and start automatically, use `default.target` +# For system level services, use `multi-user.target` +[Install] +WantedBy=multi-user.target diff --git a/server.go b/server.go new file mode 100644 index 0000000..75eeca3 --- /dev/null +++ b/server.go @@ -0,0 +1,105 @@ +package main + +import ( + "os" + "fmt" + "time" + "strings" + "strconv" + "./haproxy" + "./eosapi" + "github.com/firstrow/tcp_server" +) + +// check_api - Validates head block time. +// --------------------------------------------------------- +func check_api(host string, port int) (haproxy.HealthCheckStatus, string) { + + info, err := eosapi.GetInfo(host, port) + if err != nil { + msg := fmt.Sprintf("%s", err); + return haproxy.HealthCheckFailed, msg + } + + // Validate head block. + now := time.Now().In(time.UTC) + diff := now.Sub(info.HeadBlockTime).Seconds() + + if diff > 10.0 { + return haproxy.HealthCheckDown, + fmt.Sprintf("Taking offline because head block is lagging %.0f seconds", diff) + } else if diff < -10.0 { + return haproxy.HealthCheckDown, + fmt.Sprintf("Taking offline because head block is %.0f seconds into the future", diff) + } + return haproxy.HealthCheckUp, "OK" +} + +// argv_listen_addr +// Parse listen address from command line. +// --------------------------------------------------------- +func argv_listen_addr() string { + + var addr string + + argv := os.Args[1:] + if len(argv) > 0 { + addr = argv[0] + } else { + addr = "127.0.0.1" + } + + addr += ":" + if len(argv) > 1 { + addr += argv[1] + } else { + addr += "1337" + } + + return addr +} + +// main +// --------------------------------------------------------- +func main() { + + server := tcp_server.New(argv_listen_addr()) + + // TCP Client connect. + server.OnNewClient(func(c *tcp_server.Client) { + fmt.Println("# Client connected") + }); + + // TCP Client sends message. + server.OnNewMessage(func(c *tcp_server.Client, message string) { + var host string + var port int = 80 + + // Parse host + port. + split := strings.Split(strings.TrimSpace(message), ":") + + host = split[0] + if len(split) > 1 { + p, err := strconv.ParseInt(split[1], 10, 32) + if err == nil { + port = int(p) + } + } + + // Check api. + status, msg := check_api(host, port) + + fmt.Printf("API HealthCheck: %s, %s\n", status, msg) + + // Report status to HAproxy + c.Send(fmt.Sprintln(status)) + c.Close() + }); + + // TCP Client disconnect. + server.OnClientConnectionClosed(func(c *tcp_server.Client, err error) { + fmt.Println("# Client disconnected") + }); + + server.Listen() +}