Compare commits

...

16 Commits

Author SHA1 Message Date
Laurent Ulrich
a5a5854276 Tables are now updated only if status of host changers 2023-09-15 23:35:16 +02:00
b869e7442a race...ben j'enleve 2023-08-20 20:58:35 +02:00
72fd02704f correction erreur de build 2023-08-20 20:57:07 +02:00
dbbd7e5944 Test with shell executor 2023-08-20 20:50:23 +02:00
2b6019d465 not the right name 2023-08-20 12:00:03 +02:00
8ea021afb5 First test of gitlab CI/CD 2023-08-20 11:56:03 +02:00
6183d41d92 - Possibilité de rediriger le log vers syslog avec le flag -syslog (#1)
- Possibilité de régler le niveau de log (0,1,2) avec le flag -verbose (#2)
2023-08-18 08:08:40 +02:00
25126b2cdf Actualiser README.md 2023-08-17 21:10:04 +02:00
23d9a4c4ad Actualiser README.md 2023-08-17 21:09:18 +02:00
Laurent ULRICH
7aa75b1040 Added missing go.sum for AutoDevOps 2023-08-17 20:37:12 +02:00
b10c025860 gofmt 2023-08-17 16:14:13 +02:00
ceabd1d76b Fonctionne sur FreeBSD 2023-08-17 16:10:12 +02:00
Charlie Root
02546e58f2 modification du rc 2023-08-17 14:08:13 +00:00
1a514d3b82 added a first version of rc script for freebsd 2023-08-17 15:46:01 +02:00
d18839d64d Merge branch 'master' of https://git.passke.org/laurentu/HostChecker
Application de la licence
2023-08-17 15:23:00 +02:00
b2682db792 added license 2023-08-17 15:22:08 +02:00
5 changed files with 241 additions and 115 deletions

28
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,28 @@
# You can copy and paste this template into a new `.gitlab-ci.yml` file.
# You should not add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword.
#
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Go.gitlab-ci.yml
stages:
- test
- build
format:
stage: test
script:
- go fmt $(go list ./... | grep -v /vendor/)
- go vet $(go list ./... | grep -v /vendor/)
compile:
stage: build
script:
- mkdir -p mybinaries
- go build -o mybinaries ./...
artifacts:
paths:
- mybinaries

32
README.md Normal file
View File

@@ -0,0 +1,32 @@
HostChecker
===========
HostChecker est un outil très simpliste, qui teste la disponibilité de services distants (tcp, http, smtp) pour établir la disponibilité d'un serveur (host).
En fonction du résultat du test, il ajoute ou supprime le serveur (désigné par son IP) d'une table PF.
L'outil est écrit spécifiquement pour répondre à un besoin précis.
Sur des pare-feux réseau FreeBSD ou OpenBSD, PF (packet filter) permet de distribuer un traffic entrant vers un ou plusieurs serveurs `backends` en round-robin.
Si l'un de ces serveurs est indisponible, PF ne peut le détecter et continue de distribuer du traffic vers ce backend, générant donc des erreurs. Il existe plusieurs moyens pour pallier à ce problème, dont l'utilisation d'un proxy applicatif sur le pare-feu lui-même ou l'utilisation d'IP virtuelles redondées sur les serveurs backends.
HostChecker permet d'avoir une troisième solution, celle de gérer dynamiquement la liste des serveurs backends référencés dans une table PF.
Configuration
--------------
La configuration se fait dans un seul fichier YAML qui décrit un ou plusieurs groupes de serveurs, les méthodes de test et les tables PF a mettre à jour.
```
cluster-web:
tables:
- cluster-web-hosts
hosts:
- 10.10.1.10
- 10.10.1.11
check:
- type: http
- port: 80
- interval: 5
- timeout: 2
```

3
go.sum Normal file
View File

@@ -0,0 +1,3 @@
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,3 +1,18 @@
/*
Copyright 2023 Laurent Ulrich (laurentu@gmail.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import "fmt"
@@ -14,10 +29,11 @@ import "errors"
import "net"
import "os/exec"
import "flag"
import "log/syslog"
type Check struct {
Type string `yaml:"type"`
Host string `yaml:"host"`
Host string `yaml:"host"`
Port int `yaml:"port"`
Uri string `yaml:"uri"`
Method string `yaml:"method"`
@@ -32,13 +48,24 @@ type Group struct {
Check Check `yaml:"check"`
}
var verbose int
func main() {
log.Println("Starting")
confFileName := flag.String("f", "/usr/local/etc/hostchecker.yaml", "YAML configuration file")
flag.Parse()
if verbose > 0 {
log.Println("Starting")
}
confFileName := flag.String("f", "/usr/local/etc/hostchecker.yaml", "YAML configuration file")
flag.IntVar(&verbose, "verbose", 0, "Set logs verbosity")
useSyslog := flag.Bool("syslog", false, "Send logs to syslog")
flag.Parse()
if *useSyslog == true {
syslogWriter, err := syslog.New(syslog.LOG_ERR, "hostchecker")
if err != nil {
log.Fatal("Error opening syslog #", err)
}
log.SetOutput(syslogWriter)
}
var waitGroup sync.WaitGroup
var conf map[string]Group
@@ -51,13 +78,15 @@ func main() {
log.Fatalf("Configuration read error #%v", err)
}
err = validateConfiguration(conf)
if err != nil {
log.Fatal("Configuration error #", err)
}
err = validateConfiguration(conf)
if err != nil {
log.Fatal("Configuration error #", err)
}
stopChannel := make(chan bool)
for name, group := range conf {
log.Println("Checking group", name, group)
if verbose > 0 {
log.Println("Checking group", name, group)
}
waitGroup.Add(1)
go checkGroup(name, group, &waitGroup, stopChannel)
}
@@ -67,17 +96,23 @@ func main() {
signal.Notify(exit, os.Interrupt)
s := <-exit
log.Println("Received signal", s)
log.Println("main closing stopChannel")
if verbose > 1 {
log.Println("Received signal", s)
}
if verbose > 0 {
log.Println("main closing stopChannel")
}
close(stopChannel)
waitGroup.Wait()
}
func checkGroup(name string, group Group, waitGroup *sync.WaitGroup, stopChannel chan bool) {
channels := make(map[string]chan int)
statusPerHost := make(map[string]int)
for _, host := range group.Hosts {
channel := make(chan int, 1)
channels[host] = channel
statusPerHost[host] = -1
waitGroup.Add(1)
go checkHost(channels[host], name, host, group.Check, waitGroup, stopChannel)
}
@@ -85,19 +120,28 @@ func checkGroup(name string, group Group, waitGroup *sync.WaitGroup, stopChannel
for {
select {
case <-stopChannel:
log.Println("checkGroup", name, "stopChannel")
if verbose > 0 {
log.Println("checkGroup", name, "stopChannel")
}
waitGroup.Done()
return
break
default:
for host, channel := range channels {
select {
case stop := <-stopChannel:
log.Println("checkGroup", name, "stopChannel", stop)
if verbose > 0 {
log.Println("checkGroup", name, "stopChannel", stop)
}
break
case status := <-channel:
log.Println("Status for ", host, "is", status, "in group", name)
updateTables( host, status, group.Tables )
if verbose > 1 {
log.Println("Status for ", host, "was", statusPerHost[host], "in group", name)
log.Println("Status for ", host, "is", status, "in group", name)
}
if statusPerHost[host] == -1 || statusPerHost[host] != status {
statusPerHost[host] = status
updateTables(host, status, group.Tables)
}
default:
time.Sleep(100 * time.Millisecond)
}
@@ -106,84 +150,87 @@ func checkGroup(name string, group Group, waitGroup *sync.WaitGroup, stopChannel
}
}
func updateTables( host string, status int, tables []string ) {
for _, table := range tables {
op := "add"
if status != 0 {
op = "del"
}
cmd := exec.Command("pfctl", "-t", table, "-T", op, host)
err := cmd.Run()
if err != nil {
log.Println("Unable to run command #", cmd)
}
}
func updateTables(host string, status int, tables []string) {
for _, table := range tables {
op := "add"
if status != 0 {
op = "del"
}
cmd := exec.Command("pfctl", "-t", table, "-T", op, host)
err := cmd.Run()
if err != nil {
log.Println("Unable to run command #", cmd)
}
}
}
func checkHost(status chan<- int, group string, host string, check Check, waitGroup *sync.WaitGroup, stopChannel chan bool) {
var lastCheck time.Time
var lastCheck time.Time
for {
select {
case <-stopChannel:
log.Println("checkHost", host, "group", group, "stopChannel")
if verbose > 0 {
log.Println("checkHost", host, "group", group, "stopChannel")
}
waitGroup.Done()
return
default:
var err error
if time.Since(lastCheck).Seconds() > float64(check.Interval) {
lastCheck = time.Now()
err = nil
switch check.Type {
case "http":
err = CheckHTTP(host, check)
case "tcp":
err = CheckTCP(host, check)
case "smtp":
err = CheckSMTP(host, check)
}
if err != nil {
status <- 1
log.Println("checkHost", host, "group", group, "error", err)
} else {
status <- 0
}
}
time.Sleep(300*time.Millisecond)
if time.Since(lastCheck).Seconds() > float64(check.Interval) {
lastCheck = time.Now()
err = nil
switch check.Type {
case "http":
err = CheckHTTP(host, check)
case "tcp":
err = CheckTCP(host, check)
case "smtp":
err = CheckSMTP(host, check)
}
if err != nil {
status <- 1
if verbose > 1 {
log.Println("checkHost", host, "group", group, "error", err)
}
} else {
status <- 0
}
}
time.Sleep(300 * time.Millisecond)
}
}
}
func CheckHTTP(host string, check Check) error {
client := &http.Client{
Timeout: time.Duration(check.Timeout) * time.Second,
}
req, err := http.NewRequest(check.Method, fmt.Sprintf("http://%s%s", host, check.Uri), nil)
if err != nil {
return err
}
if len(check.Host) > 0 {
req.Header.Set("Host", check.Host)
}
if err != nil {
return err
}
if len(check.Host) > 0 {
req.Header.Set("Host", check.Host)
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
defer resp.Body.Close()
_, _ = io.ReadAll(resp.Body)
return nil
}
func CheckTCP(host string, check Check) error {
cnx, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, check.Port), time.Duration(check.Timeout) * time.Second)
if err != nil {
return err
}
cnx.Close()
cnx, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, check.Port), time.Duration(check.Timeout)*time.Second)
if err != nil {
return err
}
cnx.Close()
return nil
}
@@ -192,57 +239,61 @@ func CheckSMTP(host string, check Check) error {
}
func validateConfiguration(conf map[string]Group) error {
for name, group := range conf {
log.Println("Validating configuration", name)
if len(group.Tables) == 0 {
return errors.New(fmt.Sprintf("No tables in group %s", name))
}
log.Println("Hosts", group.Hosts)
if len(group.Hosts) == 0 {
return errors.New(fmt.Sprintf("No hosts in group %s", name))
}
for _, host := range group.Hosts {
ip := net.ParseIP(host)
if ip == nil {
return errors.New(fmt.Sprintf("Host %v is not an IP in group %s", host, name))
}
}
switch group.Check.Type {
case "http":
if len(group.Check.Method) == 0 {
group.Check.Method = "HEAD"
}
switch group.Check.Method {
case "HEAD":
case "GET":
default:
return errors.New(fmt.Sprintf("Check method shoud be HEAD or GET in group %s", name))
}
if group.Check.Port == 0 {
group.Check.Port = 80
}
if len(group.Check.Uri) == 0 {
group.Check.Uri = "/"
}
case "tcp":
if group.Check.Port == 0 {
return errors.New(fmt.Sprintf("Check port is undefined or 0 in group %s", name))
}
case "smtp":
if group.Check.Port == 0 {
group.Check.Port = 25
}
for name, group := range conf {
if verbose > 1 {
log.Println("Validating configuration", name)
}
if len(group.Tables) == 0 {
return errors.New(fmt.Sprintf("No tables in group %s", name))
}
if verbose > 1 {
log.Println("Hosts", group.Hosts)
}
if len(group.Hosts) == 0 {
return errors.New(fmt.Sprintf("No hosts in group %s", name))
}
for _, host := range group.Hosts {
ip := net.ParseIP(host)
if ip == nil {
return errors.New(fmt.Sprintf("Host %v is not an IP in group %s", host, name))
}
}
switch group.Check.Type {
case "http":
if len(group.Check.Method) == 0 {
group.Check.Method = "HEAD"
}
switch group.Check.Method {
case "HEAD":
case "GET":
default:
return errors.New(fmt.Sprintf("Check method shoud be HEAD or GET in group %s", name))
}
if group.Check.Port == 0 {
group.Check.Port = 80
}
if len(group.Check.Uri) == 0 {
group.Check.Uri = "/"
}
case "tcp":
if group.Check.Port == 0 {
return errors.New(fmt.Sprintf("Check port is undefined or 0 in group %s", name))
}
case "smtp":
if group.Check.Port == 0 {
group.Check.Port = 25
}
default:
return errors.New(fmt.Sprintf("Check type should be http, smtp or tcp in group %s",name))
}
if group.Check.Interval == 0 {
group.Check.Interval = 5
}
if group.Check.Timeout == 0 {
group.Check.Timeout = 2
}
conf[name] = group
}
return nil
default:
return errors.New(fmt.Sprintf("Check type should be http, smtp or tcp in group %s", name))
}
if group.Check.Interval == 0 {
group.Check.Interval = 5
}
if group.Check.Timeout == 0 {
group.Check.Timeout = 2
}
conf[name] = group
}
return nil
}

12
hostchecker.rc Normal file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
. /etc/rc.subr
name=hostchecker
rcvar=hostchecker_enable
command="/usr/local/bin/${name}"
command_args="&"
load_rc_config $name
run_rc_command "$1"