├── .dockerignore ├── node_test.go ├── synology-spk ├── .gitignore ├── 2_create_project │ ├── conf │ │ ├── PKG_DEPS │ │ ├── resource │ │ ├── resource.own │ │ └── privilege │ ├── scripts │ │ ├── postinst │ │ ├── postuninst │ │ ├── preinst │ │ ├── preuninst │ │ ├── preupgrade │ │ ├── postupgrade │ │ ├── frontman.sc │ │ ├── start-stop-status │ │ └── installer │ ├── PACKAGE_ICON.PNG │ ├── PACKAGE_ICON_120.PNG │ ├── PACKAGE_ICON_256.png │ ├── INFO │ ├── WIZARD_UIFILES │ │ └── install_uifile │ └── LICENSE ├── 1_create_package │ ├── ui │ │ ├── frontman-120.png │ │ ├── frontman-16.png │ │ ├── frontman-24.png │ │ ├── frontman-256.png │ │ ├── frontman-32.png │ │ ├── frontman-48.png │ │ ├── frontman-64.png │ │ ├── frontman-72.png │ │ ├── frontman-90.png │ │ └── config │ └── frontman │ │ └── frontman.default.conf ├── README.md └── create_spk.sh ├── .gitignore ├── resources ├── error.ico ├── login.png ├── logs.png ├── proxy.png └── success.ico ├── rsrc_windows.syso ├── .circleci ├── rsync-msi-build.list ├── unpublish-packages.sh ├── publish-packages.sh └── config.yml ├── syslog_fallback.go ├── pkg-scripts ├── se_linux_policy_install.sh ├── msi-templates │ ├── choco │ │ ├── LICENSE.txt │ │ ├── VERIFICATION.txt │ │ ├── chocolateyUninstall.ps1 │ │ ├── chocolateyInstall.ps1 │ │ └── pkg.nuspec │ ├── FolderUninstall.wxs │ ├── LicenseAgreementDlg_HK.wxs │ └── WixUI_HK.wxs ├── preremove-rpm.sh ├── preinstall.sh ├── preremove.sh ├── frontman.tt ├── postinstall.sh └── postinstall-rpm.sh ├── service_windows.go ├── ping_test.go ├── pkg ├── notification │ ├── toast_fallback.go │ └── toast_windows.go ├── winui │ ├── winui_fallback.go │ ├── winui_service.go │ ├── winui_logview.go │ └── winui_multipage.go ├── utils │ ├── random.go │ └── gzipreader │ │ └── gzipreader.go ├── stats │ └── stats.go └── iax │ └── iax.go ├── errors.go ├── cmd ├── mockhub │ └── main.go └── frontman │ └── main.go ├── check_test.go ├── hostinfo_test.go ├── frontman.exe.manifest ├── wix.json ├── Dockerfile ├── syslog.go ├── types.go ├── LICENSE ├── servicecheck_test.go ├── Makefile ├── go.mod ├── frontman_test.go ├── ping_ours.go ├── healthcheck.go ├── borrowed.go ├── ssl_test.go ├── service.go ├── service_notwindows.go ├── servicecheck.go ├── hostinfo.go ├── ssl.go ├── http_test.go ├── .goreleaser.yml ├── config_test.go ├── http.go ├── check.go ├── frontman.go ├── README.md ├── log.go ├── mockhub.go ├── udp.go ├── example.config.toml ├── webcheck_test.go ├── .golangci.yml ├── go.sum ├── node.go ├── example.json └── sendermode.go /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /node_test.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | -------------------------------------------------------------------------------- /synology-spk/.gitignore: -------------------------------------------------------------------------------- 1 | *.spk 2 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/conf/PKG_DEPS: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/conf/resource: -------------------------------------------------------------------------------- 1 | {"service-cfg":{}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /frontman 3 | var/ 4 | dist/ 5 | *.spk 6 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/conf/resource.own: -------------------------------------------------------------------------------- 1 | {"service-cfg":{"jobs":[]}} -------------------------------------------------------------------------------- /resources/error.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/resources/error.ico -------------------------------------------------------------------------------- /resources/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/resources/login.png -------------------------------------------------------------------------------- /resources/logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/resources/logs.png -------------------------------------------------------------------------------- /resources/proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/resources/proxy.png -------------------------------------------------------------------------------- /rsrc_windows.syso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/rsrc_windows.syso -------------------------------------------------------------------------------- /resources/success.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/resources/success.ico -------------------------------------------------------------------------------- /synology-spk/2_create_project/scripts/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . `dirname $0`/installer 3 | `basename $0` > $SYNOPKG_TEMP_LOGFILE 4 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/scripts/postuninst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . `dirname $0`/installer 3 | `basename $0` > $SYNOPKG_TEMP_LOGFILE 4 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/scripts/preinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . `dirname $0`/installer 3 | `basename $0` > $SYNOPKG_TEMP_LOGFILE 4 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/scripts/preuninst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . `dirname $0`/installer 3 | `basename $0` > $SYNOPKG_TEMP_LOGFILE 4 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/scripts/preupgrade: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . `dirname $0`/installer 3 | `basename $0` > $SYNOPKG_TEMP_LOGFILE 4 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/scripts/postupgrade: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . `dirname $0`/installer 3 | `basename $0` > $SYNOPKG_TEMP_LOGFILE 4 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/PACKAGE_ICON.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/synology-spk/2_create_project/PACKAGE_ICON.PNG -------------------------------------------------------------------------------- /synology-spk/1_create_package/ui/frontman-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/synology-spk/1_create_package/ui/frontman-120.png -------------------------------------------------------------------------------- /synology-spk/1_create_package/ui/frontman-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/synology-spk/1_create_package/ui/frontman-16.png -------------------------------------------------------------------------------- /synology-spk/1_create_package/ui/frontman-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/synology-spk/1_create_package/ui/frontman-24.png -------------------------------------------------------------------------------- /synology-spk/1_create_package/ui/frontman-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/synology-spk/1_create_package/ui/frontman-256.png -------------------------------------------------------------------------------- /synology-spk/1_create_package/ui/frontman-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/synology-spk/1_create_package/ui/frontman-32.png -------------------------------------------------------------------------------- /synology-spk/1_create_package/ui/frontman-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/synology-spk/1_create_package/ui/frontman-48.png -------------------------------------------------------------------------------- /synology-spk/1_create_package/ui/frontman-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/synology-spk/1_create_package/ui/frontman-64.png -------------------------------------------------------------------------------- /synology-spk/1_create_package/ui/frontman-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/synology-spk/1_create_package/ui/frontman-72.png -------------------------------------------------------------------------------- /synology-spk/1_create_package/ui/frontman-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/synology-spk/1_create_package/ui/frontman-90.png -------------------------------------------------------------------------------- /synology-spk/2_create_project/scripts/frontman.sc: -------------------------------------------------------------------------------- 1 | [frontman] 2 | title="frontman" 3 | desc="Frontman" 4 | port_forward="yes" 5 | dst.ports="3000/tcp" 6 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/PACKAGE_ICON_120.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/synology-spk/2_create_project/PACKAGE_ICON_120.PNG -------------------------------------------------------------------------------- /synology-spk/2_create_project/PACKAGE_ICON_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudradar/frontman/HEAD/synology-spk/2_create_project/PACKAGE_ICON_256.png -------------------------------------------------------------------------------- /.circleci/rsync-msi-build.list: -------------------------------------------------------------------------------- 1 | pkg-scripts/msi-templates/ 2 | resources/ 3 | dist/frontman_windows_amd64/frontman.exe 4 | example.config.toml 5 | example.json 6 | README.md 7 | LICENSE 8 | wix.json 9 | build-win.bat 10 | -------------------------------------------------------------------------------- /syslog_fallback.go: -------------------------------------------------------------------------------- 1 | // +build windows nacl plan9 2 | 3 | package frontman 4 | 5 | import "errors" 6 | 7 | func addSyslogHook(syslogURL string) error { 8 | return errors.New("Syslog not available for windows") 9 | } 10 | -------------------------------------------------------------------------------- /pkg-scripts/se_linux_policy_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Installing SELinux policy for frontman" 4 | checkmodule -M -m -o frontman.mod /etc/frontman/frontman.tt 5 | semodule_package -o frontman.pp -m frontman.mod 6 | semodule -i frontman.pp 7 | -------------------------------------------------------------------------------- /pkg-scripts/msi-templates/choco/LICENSE.txt: -------------------------------------------------------------------------------- 1 | From: {{.Choco.LicenseURL}} 2 | 3 | LICENSE 4 | 5 | {{if gt (.License | len) 0}} 6 | {{.License | cat}} 7 | {{else if gt (.Choco.LicenseURL | len) 0}} 8 | {{.Choco.LicenseURL | download}} 9 | {{end}} 10 | -------------------------------------------------------------------------------- /pkg-scripts/msi-templates/choco/VERIFICATION.txt: -------------------------------------------------------------------------------- 1 | VERIFICATION 2 | 3 | To check the checksum of this package, extract the msi file contained into it, 4 | then run 5 | 6 | checksum.exe {{.Choco.MsiFile}} -t=sha256 7 | 8 | The result must match 9 | 10 | {{.Choco.MsiSum | upper}} 11 | -------------------------------------------------------------------------------- /pkg-scripts/msi-templates/choco/chocolateyUninstall.ps1: -------------------------------------------------------------------------------- 1 | $packageName = "{{.Choco.ID}}"; 2 | $fileType = 'msi'; 3 | $scriptPath = $(Split-Path $MyInvocation.MyCommand.Path); 4 | $fileFullPath = Join-Path $scriptPath '{{.Choco.MsiFile}}'; 5 | 6 | Uninstall-ChocolateyPackage $packageName $fileType "$fileFullPath /q" 7 | -------------------------------------------------------------------------------- /pkg-scripts/preremove-rpm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Removing during upgrade: 1 or higher 4 | # Remove last version of package: 0 5 | versionsCount=$1 6 | 7 | # we need to uninstall service only if we are removing the last packages 8 | if [ ${versionsCount} -lt 1 ]; then 9 | /usr/bin/frontman -u || true 10 | fi -------------------------------------------------------------------------------- /service_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package frontman 4 | 5 | import ( 6 | "github.com/kardianos/service" 7 | ) 8 | 9 | func updateServiceConfig(fm *Frontman, username string) { 10 | // nothing to do 11 | } 12 | 13 | func (fm *Frontman) configureServiceEnabledState(s service.Service) { 14 | // nothing to do 15 | } 16 | -------------------------------------------------------------------------------- /ping_test.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPing(t *testing.T) { 10 | cfg, err := HandleAllConfigSetup(DefaultCfgPath) 11 | 12 | cfg.ICMPTimeout = 0.2 13 | assert.Nil(t, err) 14 | fm := helperCreateFrontman(t, cfg) 15 | 16 | _, _ = fm.runPing("8.8.8.8") 17 | } 18 | -------------------------------------------------------------------------------- /synology-spk/README.md: -------------------------------------------------------------------------------- 1 | # frontman-spk 2 | 3 | Frontman SPK package ([Synology PacKages](https://www.synology.com/en-us/dsm/app_packages)) 4 | 5 | Install Frontman into a Synology NAS. 6 | 7 | ## Usage 8 | 9 | Change **Package Center -> Trust Level** to **Any Publisher** and import manually the package from **Manual install**. 10 | Finally, configure service using SSH. 11 | -------------------------------------------------------------------------------- /pkg/notification/toast_fallback.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package notification 4 | 5 | import ( 6 | "errors" 7 | ) 8 | 9 | func SendErrorNotification(_, _ string) error { 10 | return errors.New("implemented only or Windows") 11 | } 12 | 13 | func SendSuccessNotification(_, _ string) error { 14 | return errors.New("implemented only or Windows") 15 | } 16 | -------------------------------------------------------------------------------- /pkg-scripts/msi-templates/choco/chocolateyInstall.ps1: -------------------------------------------------------------------------------- 1 | $packageName = '{{.Choco.ID}}' 2 | $fileType = 'msi' 3 | $silentArgs = '/quiet'; 4 | $scriptPath = $(Split-Path $MyInvocation.MyCommand.Path); 5 | $fileFullPath = Join-Path $scriptPath '{{.Choco.MsiFile}}'; 6 | 7 | Install-ChocolateyInstallPackage $packageName $fileType $silentArgs $fileFullPath -checksum '{{.Choco.MsiSum}}' -checksumType = 'sha256' 8 | -------------------------------------------------------------------------------- /synology-spk/1_create_package/ui/config: -------------------------------------------------------------------------------- 1 | { 2 | ".url": { 3 | "io.cloudradar.frontman": { 4 | "type": "url", 5 | "allUsers": true, 6 | "title": "Frontman", 7 | "desc": "Monitoring proxy for agentless monitoring of subnets", 8 | "icon": "frontman-{0}.png", 9 | "grantPrivilege": "local" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import "github.com/pkg/errors" 4 | 5 | func newEmptyFieldError(name string) error { 6 | err := errors.Errorf("unexpected empty field %s", name) 7 | return errors.Wrap(err, "the field must be filled with details of your Cloudradar account") 8 | } 9 | 10 | func newFieldError(name string, err error) error { 11 | return errors.Wrapf(err, "%s field verification failed", name) 12 | } 13 | -------------------------------------------------------------------------------- /.circleci/unpublish-packages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xe 4 | 5 | ssh_cr() { 6 | ssh -p 24480 -oStrictHostKeyChecking=no cr@repo.cloudradar.io "$@" 7 | } 8 | 9 | ssh_cr /home/cr/work/msi/feed_delete.sh frontman rolling ${CIRCLE_TAG} 10 | ssh_cr /home/cr/work/msi/feed_delete.sh frontman stable ${CIRCLE_TAG} 11 | 12 | github-release delete --user cloudradar-monitoring --repo frontman --tag ${CIRCLE_TAG} 13 | 14 | 15 | -------------------------------------------------------------------------------- /pkg/winui/winui_fallback.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package winui 4 | 5 | import ( 6 | "github.com/cloudradar-monitoring/frontman" 7 | ) 8 | 9 | // WindowsShowSettingsUI stub exists only for cross-platform compiling on platforms that don't implement Windows UI. 10 | func WindowsShowSettingsUI(_ *frontman.Frontman, _ bool) { 11 | 12 | } 13 | 14 | func HandleFeedback(fm *frontman.Frontman, cfgPath string) { 15 | } 16 | -------------------------------------------------------------------------------- /pkg/utils/random.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "math/rand" 4 | 5 | var letters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 6 | 7 | // randomizedStr returns string of len strLen 8 | // probability of all the letters will not be exactly the same 9 | func RandomizedStr(strLen int) string { 10 | b := make([]byte, strLen) 11 | for i := range b { 12 | b[i] = letters[rand.Int63()%int64(len(letters))] 13 | } 14 | return string(b) 15 | } 16 | -------------------------------------------------------------------------------- /cmd/mockhub/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/cloudradar-monitoring/frontman" 9 | ) 10 | 11 | func main() { 12 | 13 | rand.Seed(time.Now().UnixNano()) 14 | 15 | responseCode := flag.Int("code", 0, "response code") 16 | flag.Parse() 17 | 18 | hub := frontman.NewMockHub("0.0.0.0:9100") 19 | if *responseCode != 0 { 20 | hub.ResponseStatusCode = *responseCode 21 | } 22 | hub.Serve() 23 | } 24 | -------------------------------------------------------------------------------- /pkg-scripts/preinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # check that owner group exists 4 | if [ -z `getent group frontman` ]; then 5 | groupadd frontman 6 | fi 7 | 8 | # check that user exists 9 | if [ -z `getent passwd frontman` ]; then 10 | useradd --gid frontman --system --shell /bin/false frontman 11 | fi 12 | 13 | # remove deprecated sysctl setting 14 | if [ -e /etc/sysctl.d/50-ping_group_range.conf ]; then 15 | rm -f /etc/sysctl.d/50-ping_group_range.conf 16 | fi 17 | -------------------------------------------------------------------------------- /pkg-scripts/preremove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | case "$1" in 4 | remove) 5 | # remove service only when removing package (not update) 6 | /usr/bin/frontman -u 7 | ;; 8 | upgrade) 9 | # do not stop service on package upgrade because it will be restarted by new package' postinst script 10 | ;; 11 | *) 12 | echo "stopping service..." 13 | /usr/bin/frontman -service_stop || true 14 | ;; 15 | esac 16 | 17 | exit 0 -------------------------------------------------------------------------------- /check_test.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestInProgressChecks(t *testing.T) { 10 | 11 | ipc := newIPC() 12 | 13 | ipc.add("one") 14 | ipc.add("two") 15 | assert.Equal(t, true, ipc.isInProgress("one")) 16 | assert.Equal(t, true, ipc.isInProgress("two")) 17 | assert.Equal(t, false, ipc.isInProgress("three")) 18 | 19 | ipc.remove("two") 20 | assert.Equal(t, false, ipc.isInProgress("two")) 21 | } 22 | -------------------------------------------------------------------------------- /pkg-scripts/frontman.tt: -------------------------------------------------------------------------------- 1 | # SELinux policy for frontman 2 | 3 | module frontman 1.0; 4 | 5 | require { 6 | type rpm_script_t; 7 | type node_t; 8 | type unconfined_service_t; 9 | type unconfined_t; 10 | class icmp_socket node_bind; 11 | } 12 | 13 | #============= rpm_script_t ============== 14 | allow rpm_script_t node_t:icmp_socket node_bind; 15 | 16 | #============= unconfined_service_t ============== 17 | allow unconfined_service_t node_t:icmp_socket node_bind; 18 | 19 | #============= unconfined_t ============== 20 | allow unconfined_t node_t:icmp_socket node_bind; 21 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/INFO: -------------------------------------------------------------------------------- 1 | package="Frontman" 2 | version="{PKG_VERSION}" 3 | description="Monitoring proxy for agentless monitoring of subnets" 4 | support_url="https://github.com/cloudradar-monitoring/frontman/" 5 | displayname="Frontman" 6 | maintainer="CloudRadar" 7 | maintainer_url="https://cloudradar.io" 8 | distributor="CloudRadar" 9 | distributor_url="https://cloudradar.io" 10 | arch="{PKG_ARCH}" 11 | dsmuidir="ui" 12 | checkport="yes" 13 | dsmappname="io.cloudradar.frontman" 14 | support_center="yes" 15 | install_dep_services="" 16 | start_dep_services="" 17 | support_conf_folder="yes" 18 | -------------------------------------------------------------------------------- /synology-spk/1_create_package/frontman/frontman.default.conf: -------------------------------------------------------------------------------- 1 | # This is an auto-generated config to connect with the cloudradar service 2 | # To see all options of frontman run frontman -p 3 | 4 | # "debug", "info", "error" verbose level; can be overridden with -v flag 5 | log_level = "error" 6 | 7 | # "file" or "http" – where frontman gets checks to perform and post results 8 | io_mode = "http" 9 | 10 | hub_url = "CONFIG_HUB_URL" 11 | hub_user = "CONFIG_HUB_USER" 12 | hub_password = "CONFIG_HUB_PASSWORD" 13 | 14 | host_info = ["uname", "os_kernel", "os_family", "os_arch", "cpu_model", "fqdn", "memory_total_B"] 15 | -------------------------------------------------------------------------------- /hostinfo_test.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestHostInfoResults(t *testing.T) { 11 | cfg, err := HandleAllConfigSetup(DefaultCfgPath) 12 | assert.Nil(t, err) 13 | 14 | // system_fields is deprecated, but we test to make sure system_fields is treated as host_info 15 | cfg.HostInfo = []string{"uname", "os_kernel", "os_family", "os_arch", "cpu_model", "fqdn"} 16 | cfg.SystemFields = []string{"hostname", "memory_total_B"} 17 | fm := helperCreateFrontman(t, cfg) 18 | 19 | v, err := fm.HostInfoResults() 20 | assert.Nil(t, err) 21 | assert.Equal(t, 8, len(v)) 22 | fmt.Printf("%+v\n", v) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/utils/gzipreader/gzipreader.go: -------------------------------------------------------------------------------- 1 | package gzipreader 2 | 3 | import ( 4 | "compress/gzip" 5 | "io" 6 | ) 7 | 8 | // GzipReader wraps a ReadCloser 9 | // call gzip.NewReader on the first call to Read 10 | type GzipReader struct { 11 | Reader io.ReadCloser 12 | zreader *gzip.Reader 13 | zerr error 14 | } 15 | 16 | func (gz *GzipReader) Read(p []byte) (n int, err error) { 17 | if gz.zreader == nil { 18 | if gz.zerr == nil { 19 | gz.zreader, gz.zerr = gzip.NewReader(gz.Reader) 20 | } 21 | if gz.zerr != nil { 22 | return 0, gz.zerr 23 | } 24 | } 25 | 26 | return gz.zreader.Read(p) 27 | } 28 | 29 | func (gz *GzipReader) Close() error { 30 | return gz.Reader.Close() 31 | } 32 | -------------------------------------------------------------------------------- /pkg/stats/stats.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import "time" 4 | 5 | // FrontmanStats holds application stats 6 | type FrontmanStats struct { 7 | BytesSentToHubTotal uint64 8 | BytesFetchedFromHubTotal uint64 9 | 10 | ChecksPerformedTotal uint64 11 | ChecksFetchedFromHub uint64 12 | CheckResultsSentToHub uint64 13 | 14 | HubErrorsTotal uint64 15 | HubLastErrorMessage string 16 | HubLastErrorTimestamp uint64 17 | 18 | InternalErrorsTotal uint64 19 | InternalLastErrorMessage string 20 | InternalLastErrorTimestamp uint64 21 | 22 | HealthChecksPerformed uint64 23 | HealthChecksLastTimestamp uint64 24 | 25 | Uptime uint64 26 | StartedAt time.Time `json:"-"` // Used to calculate Uptime 27 | } 28 | -------------------------------------------------------------------------------- /frontman.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | true 12 | 13 | 14 | -------------------------------------------------------------------------------- /wix.json: -------------------------------------------------------------------------------- 1 | { 2 | "product": "frontman", 3 | "company": "cloudradar", 4 | "license": "LICENSE", 5 | "upgrade-code": "72fad937-0991-4c67-996c-397aae55d475", 6 | "files": { 7 | "guid": "fb1d0c2a-f807-4f46-8c58-08f68847d889", 8 | "items": [ 9 | "frontman.exe", 10 | "example.config.toml", 11 | "example.json" 12 | ] 13 | }, 14 | "directories": [ 15 | "resources" 16 | ], 17 | "env": { 18 | "guid": "43e89972-ea2f-4b01-b4c6-87d6929d5015", 19 | "vars": [ 20 | ] 21 | }, 22 | "choco": { 23 | "description": "Monitoring proxy for agentless monitoring of subnets", 24 | "project-url": "https://github.com/cloudradar-monitoring/frontman", 25 | "license-url": "https://github.com/cloudradar-monitoring/frontman/blob/master/LICENSE" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.9 2 | 3 | ARG FRONTMAN_VERSION 4 | 5 | ENV FRONTMAN_HUB_URL=https://hub.cloudradar.io/checks/ 6 | # User and password should be passed via -e when starting the container 7 | #ENV FRONTMAN_HUB_USER=XXXXXXXXX 8 | #ENV FRONTMAN_HUB_PASSWORD=XXXXXXXX 9 | 10 | RUN apk update && apk add ca-certificates 11 | 12 | RUN wget https://github.com/cloudradar-monitoring/frontman/releases/download/${FRONTMAN_VERSION}/frontman_${FRONTMAN_VERSION}_Linux_x86_64.tar.gz && \ 13 | tar xf frontman_${FRONTMAN_VERSION}_Linux_x86_64.tar.gz && \ 14 | mv frontman /usr/local/bin/ && \ 15 | mkdir /etc/frontman && \ 16 | mv example.config.toml /etc/frontman/config.toml && \ 17 | rm -rf frontman_${FRONTMAN_VERSION}_Linux_x86_64 && \ 18 | rm frontman_${FRONTMAN_VERSION}_Linux_x86_64.tar.gz 19 | 20 | CMD ["/usr/local/bin/frontman"] 21 | -------------------------------------------------------------------------------- /syslog.go: -------------------------------------------------------------------------------- 1 | // +build !windows,!nacl,!plan9 2 | // OS list copied from log/syslog 3 | 4 | package frontman 5 | 6 | import ( 7 | "fmt" 8 | "log/syslog" 9 | "net/url" 10 | 11 | "github.com/sirupsen/logrus" 12 | lSyslog "github.com/sirupsen/logrus/hooks/syslog" 13 | ) 14 | 15 | func addSyslogHook(syslogURL string) error { 16 | 17 | var network, raddr string 18 | 19 | if syslogURL != "local" { 20 | u, err := url.Parse(syslogURL) 21 | if err != nil { 22 | return fmt.Errorf("wrong format of syslogURL: %s", err.Error()) 23 | } 24 | network = u.Scheme 25 | raddr = u.Host 26 | 27 | if u.Port() == "" { 28 | raddr += ":;514" 29 | } 30 | } 31 | 32 | hook, err := lSyslog.NewSyslogHook(network, raddr, syslog.LOG_DEBUG, "frontman") 33 | 34 | if err != nil { 35 | return err 36 | } 37 | 38 | logrus.AddHook(hook) 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/conf/privilege: -------------------------------------------------------------------------------- 1 | { 2 | "username": "frontman", 3 | "groupname": "frontman", 4 | "ctrl-script": [{ 5 | "action": "preinst", 6 | "run-as": "root" 7 | }, 8 | { 9 | "action": "postinst", 10 | "run-as": "root" 11 | }, 12 | { 13 | "action": "preuninst", 14 | "run-as": "root" 15 | }, 16 | { 17 | "action": "postuninst", 18 | "run-as": "root" 19 | }, 20 | { 21 | "action": "preupgrade", 22 | "run-as": "root" 23 | }, 24 | { 25 | "action": "postupgrade", 26 | "run-as": "root" 27 | }, 28 | { 29 | "action": "start", 30 | "run-as": "package" 31 | }, 32 | { 33 | "action": "stop", 34 | "run-as": "root" 35 | }, 36 | { 37 | "action": "status", 38 | "run-as": "root" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | type ServiceName string 4 | 5 | const ( 6 | ProtocolICMP = "icmp" 7 | ProtocolTCP = "tcp" 8 | ProtocolUDP = "udp" 9 | ProtocolSSL = "ssl" 10 | 11 | ServiceICMPPing = "ping" 12 | ) 13 | 14 | type Results struct { 15 | Results []Result `json:"results"` 16 | HostInfo map[string]interface{} `json:"hostInfo,omitempty"` 17 | } 18 | 19 | type Result struct { 20 | CheckUUID string `json:"checkUuid"` 21 | Timestamp int64 `json:"timestamp"` 22 | CheckType string `json:"checkType"` 23 | Check interface{} `json:"check"` // *CheckData 24 | Measurements map[string]interface{} `json:"measurements"` 25 | Message interface{} `json:"message"` 26 | Node string `json:"node,omitempty"` // filled in when result is coming from a neighbor 27 | NodeMeasurements []map[string]interface{} `json:"nodeMeasurements,omitempty"` 28 | } 29 | 30 | type MeasurementsMap map[string]interface{} 31 | -------------------------------------------------------------------------------- /pkg-scripts/msi-templates/FolderUninstall.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/WIZARD_UIFILES/install_uifile: -------------------------------------------------------------------------------- 1 | [{ 2 | "step_title": "Frontman configuration", 3 | "items": [{ 4 | "type": "textfield", 5 | "desc": "Configure frontman authentication for Cloudradar Hub", 6 | "subitems": [{ 7 | "key": "CONFIG_HUB_URL", 8 | "desc": "Hub URL", 9 | "defaultValue": "https://hub.cloudradar.io/checks/", 10 | "validator": { 11 | "allowBlank": false, 12 | "minLength": 2, 13 | "maxLength": 100 14 | } 15 | },{ 16 | "key": "CONFIG_HUB_USER", 17 | "desc": "Hub Username", 18 | "validator": { 19 | "allowBlank": false, 20 | "minLength": 2, 21 | "maxLength": 40 22 | } 23 | },{ 24 | "key": "CONFIG_HUB_PASSWORD", 25 | "desc": "Hub Password", 26 | "validator": { 27 | "allowBlank": false, 28 | "minLength": 2, 29 | "maxLength": 40 30 | } 31 | }] 32 | }] 33 | }] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 cloudradar-monitoring 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Cloudradar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /servicecheck_test.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestDNSUDPCheck(t *testing.T) { 10 | cfg, _ := HandleAllConfigSetup(DefaultCfgPath) 11 | cfg.Sleep = 10 12 | fm := helperCreateFrontman(t, cfg) 13 | input := &Input{ 14 | ServiceChecks: []ServiceCheck{{ 15 | UUID: "dns-udp-check", 16 | Check: ServiceCheckData{ 17 | Connect: "8.8.8.8", 18 | Protocol: "udp", 19 | Service: "dns", 20 | Port: "53", 21 | }, 22 | }}, 23 | } 24 | fm.processInput(input.asChecks(), true) 25 | res := <-fm.resultsChan 26 | require.Equal(t, nil, res.Message) 27 | require.Equal(t, 1, res.Measurements["net.udp.dns.53.success"]) 28 | } 29 | 30 | func TestDNSTCPCheck(t *testing.T) { 31 | cfg, _ := HandleAllConfigSetup(DefaultCfgPath) 32 | cfg.Sleep = 10 33 | fm := helperCreateFrontman(t, cfg) 34 | input := &Input{ 35 | ServiceChecks: []ServiceCheck{{ 36 | UUID: "dns-tcp-check", 37 | Check: ServiceCheckData{ 38 | Connect: "8.8.8.8", 39 | Protocol: "tcp", 40 | Service: "dns", 41 | Port: "53", 42 | }, 43 | }}, 44 | } 45 | fm.processInput(input.asChecks(), true) 46 | res := <-fm.resultsChan 47 | require.Equal(t, nil, res.Message) 48 | require.Equal(t, 1, res.Measurements["net.tcp.dns.53.success"]) 49 | } 50 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | 3 | PROJECT_DIR=/go/src/github.com/cloudradar-monitoring/frontman 4 | 5 | ifeq ($(RELEASE_MODE),) 6 | RELEASE_MODE=release-candidate 7 | endif 8 | ifeq ($(RELEASE_MODE),release-candidate) 9 | SELF_UPDATES_FEED_URL="https://repo.cloudradar.io/windows/frontman/feed/rolling" 10 | endif 11 | ifeq ($(RELEASE_MODE),stable) 12 | SELF_UPDATES_FEED_URL="https://repo.cloudradar.io/windows/frontman/feed/stable" 13 | endif 14 | 15 | # Go parameters 16 | GOCMD=go 17 | GOBUILD=$(GOCMD) build 18 | GOCLEAN=$(GOCMD) clean 19 | GOTEST=$(GOCMD) test 20 | GOGET=$(GOCMD) get 21 | GORUN=$(GOCMD) run 22 | BINARY_NAME=frontman 23 | 24 | all: test build 25 | 26 | build: 27 | $(GOBUILD) -v ./cmd/frontman/... 28 | 29 | test: 30 | $(GOTEST) -v ./... 31 | 32 | test-short: 33 | $(GOTEST) -short -v ./... 34 | 35 | clean: 36 | $(GOCLEAN) 37 | rm -f $(BINARY_NAME) 38 | 39 | run: 40 | $(GORUN) -v ./cmd/frontman/... 41 | 42 | goimports: 43 | goimports -l $$(find . -type f -name '*.go' -not -path "./vendor/*") 44 | 45 | goreleaser-precheck: 46 | @if [ -z ${SELF_UPDATES_FEED_URL} ]; then echo "SELF_UPDATES_FEED_URL is empty"; exit 1; fi 47 | 48 | goreleaser-rm-dist: goreleaser-precheck 49 | SELF_UPDATES_FEED_URL=$(SELF_UPDATES_FEED_URL) goreleaser --rm-dist 50 | 51 | goreleaser-snapshot: goreleaser-precheck 52 | SELF_UPDATES_FEED_URL=$(SELF_UPDATES_FEED_URL) goreleaser --snapshot --rm-dist 53 | 54 | -------------------------------------------------------------------------------- /pkg/notification/toast_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package notification 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | 9 | "gopkg.in/toast.v1" 10 | ) 11 | 12 | const toastErrorIcon = "resources\\error.png" 13 | const toastSuccessIcon = "resources\\success.png" 14 | const toastAppID = "cloudradar.frontman" 15 | 16 | func getExecutablePath() string { 17 | ex, err := os.Executable() 18 | if err != nil { 19 | return "" 20 | } 21 | 22 | return filepath.Dir(ex) 23 | } 24 | 25 | func SendErrorNotification(title, message string) error { 26 | msg := toast.Notification{ 27 | AppID: toastAppID, 28 | Title: title, 29 | Message: message, 30 | Duration: toast.Long, // last for 25sec 31 | Actions: []toast.Action{{ 32 | Type: "protocol", 33 | Label: "Open settings", 34 | Arguments: "frontman:settings", 35 | }}} 36 | 37 | iconPath := getExecutablePath() + "\\" + toastErrorIcon 38 | if _, err := os.Stat(iconPath); err == nil { 39 | msg.Icon = iconPath 40 | } 41 | return msg.Push() 42 | } 43 | 44 | func SendSuccessNotification(title, message string) error { 45 | msg := toast.Notification{ 46 | AppID: toastAppID, 47 | Title: title, 48 | Message: message, 49 | Duration: toast.Long, // last for 25sec 50 | Actions: []toast.Action{}, 51 | } 52 | 53 | iconPath := getExecutablePath() + "\\" + toastSuccessIcon 54 | if _, err := os.Stat(iconPath); err == nil { 55 | msg.Icon = iconPath 56 | } 57 | return msg.Push() 58 | } 59 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudradar-monitoring/frontman 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect 7 | github.com/cloudradar-monitoring/selfupdate v0.0.0-20200615195818-3bc6d247a637 8 | github.com/cloudradar-monitoring/toml v0.4.3-0.20190904091934-b07890c4335d 9 | github.com/go-ole/go-ole v1.2.4 // indirect 10 | github.com/go-ping/ping v0.0.0-20201022122018-3977ed72668a 11 | github.com/golang/mock v1.4.4 // indirect 12 | github.com/gorilla/handlers v1.5.1 13 | github.com/kardianos/service v1.2.0 14 | github.com/lxn/walk v0.0.0-20190515104301-6cf0bf1359a5 15 | github.com/lxn/win v0.0.0-20190514122436-6f00d814e89c 16 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect 17 | github.com/pkg/errors v0.9.1 18 | github.com/shirou/gopsutil v2.20.9+incompatible 19 | github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 20 | github.com/sirupsen/logrus v1.7.0 21 | github.com/soniah/gosnmp v1.21.1-0.20190510081145-1b12be15031c 22 | github.com/stretchr/testify v1.6.1 23 | golang.org/x/net v0.0.0-20201029055024-942e2f445f3c 24 | golang.org/x/sys v0.0.0-20201214095126-aec9a390925b 25 | gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect 26 | gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect 27 | gopkg.in/ldap.v3 v3.0.3 28 | gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 29 | ) 30 | 31 | replace github.com/kardianos/service v1.0.1-0.20190514155156-fffe6c52ed0f => github.com/cloudradar-monitoring/service v1.0.1-0.20190819150840-489f2db8fe1e 32 | -------------------------------------------------------------------------------- /frontman_test.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func helperCreateFrontman(t *testing.T, cfg *Config) *Frontman { 11 | t.Helper() 12 | fm, err := New(cfg, DefaultCfgPath, "1.2.3") 13 | assert.Nil(t, err) 14 | fm.ipc = newIPC() 15 | return fm 16 | } 17 | 18 | func TestFrontmanHubInput(t *testing.T) { 19 | hub := NewMockHub("localhost:9100") 20 | go hub.Serve() 21 | 22 | cfg, err := HandleAllConfigSetup(DefaultCfgPath) 23 | assert.Nil(t, err) 24 | 25 | cfg.HubURL = hub.URL() + "/?serviceChecks=1&webChecks=1" 26 | cfg.LogLevel = "debug" 27 | cfg.Sleep = 10 // delay between each round of checks 28 | cfg.SenderBatchSize = 2 // number of results to send to hub at once 29 | cfg.SenderInterval = 0 30 | cfg.ICMPTimeout = 0.1 31 | cfg.HTTPCheckTimeout = 0.1 32 | cfg.SleepDurationAfterCheck = 0 33 | cfg.SleepDurationEmptyQueue = 0 34 | cfg.Nodes = make(map[string]Node) 35 | 36 | fm := helperCreateFrontman(t, cfg) 37 | 38 | go fm.Run("", nil) 39 | 40 | // stop after some time 41 | time.Sleep(2000 * time.Millisecond) 42 | close(fm.InterruptChan) 43 | 44 | fm.statsLock.Lock() 45 | assert.Equal(t, true, fm.stats.BytesSentToHubTotal > 0) 46 | assert.Equal(t, true, fm.stats.BytesFetchedFromHubTotal > 0) 47 | assert.Equal(t, true, fm.stats.ChecksPerformedTotal > 0) 48 | assert.Equal(t, uint64(2), fm.stats.ChecksFetchedFromHub) 49 | assert.Equal(t, true, fm.stats.CheckResultsSentToHub > 0) 50 | fm.statsLock.Unlock() 51 | 52 | fm.ipc.mutex.RLock() 53 | assert.Equal(t, true, len(fm.ipc.uuids) > 0) 54 | fm.ipc.mutex.RUnlock() 55 | } 56 | -------------------------------------------------------------------------------- /ping_ours.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "runtime" 5 | "time" 6 | 7 | "github.com/go-ping/ping" 8 | 9 | "github.com/pkg/errors" 10 | "golang.org/x/net/icmp" 11 | ) 12 | 13 | func CheckIfRawICMPAvailable() bool { 14 | conn, err := icmp.ListenPacket("ip4:1", "0.0.0.0") 15 | if err != nil { 16 | return false 17 | } 18 | 19 | conn.Close() 20 | return true 21 | } 22 | 23 | func CheckIfRootlessICMPAvailable() bool { 24 | conn, err := icmp.ListenPacket("udp4", "") 25 | if err != nil { 26 | return false 27 | } 28 | 29 | conn.Close() 30 | return true 31 | } 32 | 33 | func (fm *Frontman) runPing(addr string) (m map[string]interface{}, err error) { 34 | prefix := "net.icmp.ping." 35 | m = make(map[string]interface{}) 36 | 37 | pinger, err := ping.NewPinger(addr) 38 | if err != nil { 39 | return 40 | } 41 | 42 | if CheckIfRawICMPAvailable() || runtime.GOOS == "windows" { 43 | pinger.SetPrivileged(true) 44 | } 45 | 46 | pinger.Timeout = secToDuration(fm.Config.ICMPTimeout) 47 | pinger.Count = 5 48 | 49 | pinger.Run() 50 | 51 | var total time.Duration 52 | 53 | stats := pinger.Statistics() 54 | for _, rtt := range stats.Rtts { 55 | total += rtt 56 | } 57 | 58 | if pinger.PacketsSent > 0 { 59 | m[prefix+"packetLoss_percent"] = float64(pinger.PacketsSent-pinger.PacketsRecv) / float64(pinger.PacketsSent) * 100 60 | } 61 | if (len(stats.Rtts)) > 0 { 62 | m[prefix+"roundTripTime_s"] = total.Seconds() / float64(len(stats.Rtts)) 63 | } 64 | success := 0 65 | if pinger.PacketsRecv > 0 { 66 | success = 1 67 | } else { 68 | err = errors.New("no packets received") 69 | } 70 | 71 | m[prefix+"success"] = success 72 | 73 | return 74 | } 75 | -------------------------------------------------------------------------------- /pkg-scripts/msi-templates/choco/pkg.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{.Choco.ID}} 5 | {{.Choco.Title}} 6 | {{.VersionOk}} 7 | {{.Choco.Authors}} 8 | {{.Choco.Owners}} 9 | {{.Choco.Description}} 10 | {{if gt (.Choco.ProjectURL | len) 0}} 11 | {{.Choco.ProjectURL}} 12 | {{end}} 13 | {{if gt (.Choco.Tags | len) 0}} 14 | {{.Choco.Tags}} 15 | {{end}} 16 | {{if gt (.Choco.LicenseURL | len) 0}} 17 | {{.Choco.LicenseURL}} 18 | {{end}} 19 | {{if gt (.Choco.IconURL | len) 0}} 20 | {{.Choco.IconURL}} 21 | {{end}} 22 | {{if gt (.Choco.ChangeLog | len) 0}} 23 | {{.Choco.ChangeLog}} 24 | {{end}} 25 | {{if .Choco.RequireLicense}} 26 | true 27 | {{else}} 28 | false 29 | {{end}} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /pkg-scripts/postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | CONFIG_PATH=/etc/frontman/frontman.conf 4 | 5 | # give frontman icmp ping rights 6 | if which setcap&>/dev/null;then 7 | setcap cap_net_raw=+ep /usr/bin/frontman 8 | fi 9 | 10 | if [ "$1" = configure ]; then 11 | 12 | # $2 contains previous version number 13 | if [ -z "$2" ]; then # fresh install 14 | /usr/bin/frontman -y -s frontman -c ${CONFIG_PATH} 15 | else # package update 16 | serviceStatus=`/usr/bin/frontman -y -service_status -c ${CONFIG_PATH}` 17 | echo "current service status: $serviceStatus." 18 | 19 | case "$serviceStatus" in 20 | unknown|failed) 21 | echo "trying to repair service..." 22 | /usr/bin/frontman -u || true 23 | /usr/bin/frontman -y -s frontman -c ${CONFIG_PATH} 24 | ;; 25 | 26 | running|stopped) 27 | # try to upgrade service unit config 28 | 29 | if [ "$serviceStatus" = running ]; then 30 | echo "stopping service..." 31 | /usr/bin/frontman -service_stop || true 32 | fi 33 | 34 | echo "upgrading service unit... " 35 | /usr/bin/frontman -y -s frontman -service_upgrade -c ${CONFIG_PATH} 36 | 37 | # restart only if it was active before 38 | if [ "$serviceStatus" = running ]; then 39 | echo "starting service... " 40 | /usr/bin/frontman -y -service_start -c ${CONFIG_PATH} 41 | fi 42 | ;; 43 | 44 | *) 45 | echo "unknown service status. Exiting..." 46 | exit 1 47 | ;; 48 | esac 49 | fi 50 | fi 51 | 52 | /usr/bin/frontman -t || true 53 | -------------------------------------------------------------------------------- /healthcheck.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | "time" 8 | 9 | "github.com/go-ping/ping" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // HealthCheck runs before any other check to ensure that the host itself and its network are healthly. 14 | // This is useful to confirm a stable internet connection to avoid false alerts due to network outages. 15 | func (fm *Frontman) HealthCheck() error { 16 | hcfg := fm.Config.HealthChecks 17 | if len(hcfg.ReferencePingHosts) == 0 { 18 | return nil 19 | } 20 | if hcfg.ReferencePingCount == 0 { 21 | return nil 22 | } 23 | timeout := secToDuration(hcfg.ReferencePingTimeout) 24 | if timeout == 0 { 25 | // use the default timeout 26 | timeout = 500 * time.Millisecond 27 | } 28 | failC := make(chan string, len(hcfg.ReferencePingHosts)) 29 | 30 | wg := new(sync.WaitGroup) 31 | for _, addr := range hcfg.ReferencePingHosts { 32 | pinger, err := ping.NewPinger(addr) 33 | if err != nil { 34 | logrus.WithError(err).Warningln("failed to parse host for ICMP ping") 35 | continue 36 | } 37 | pinger.Timeout = timeout 38 | pinger.Count = hcfg.ReferencePingCount 39 | wg.Add(1) 40 | go func(addr string) { 41 | defer wg.Done() 42 | pinger.Run() 43 | if pinger.Statistics().PacketLoss > 0 { 44 | failC <- addr 45 | } 46 | }(addr) 47 | } 48 | go func() { 49 | wg.Wait() 50 | close(failC) 51 | }() 52 | 53 | failedHosts := []string{} 54 | for host := range failC { 55 | failedHosts = append(failedHosts, host) 56 | } 57 | 58 | fm.statsLock.Lock() 59 | fm.stats.HealthChecksPerformed++ 60 | fm.stats.HealthChecksLastTimestamp = uint64(time.Now().Unix()) 61 | fm.statsLock.Unlock() 62 | 63 | if len(failedHosts) > 0 { 64 | return fmt.Errorf("host(s) failed to respond to ICMP ping: %s", strings.Join(failedHosts, ", ")) 65 | } 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /pkg/iax/iax.go: -------------------------------------------------------------------------------- 1 | package iax 2 | 3 | /* IAX packet structure: 4 | struct ast_iax2_full_hdr { 5 | unsigned short scallno; // Source call number -- high bit must be 1 6 | unsigned short dcallno; // Destination call number -- high bit is 1 if retransmission 7 | unsigned int ts; // 32-bit timestamp in milliseconds (from 1st transmission) 8 | unsigned char oseqno; // Packet number (outgoing) 9 | unsigned char iseqno; // Packet number (next incoming expected) 10 | unsigned char type; // Frame type 11 | unsigned char csub; // Compressed subclass 12 | unsigned char iedata[0]; 13 | } __attribute__ ((__packed__)); 14 | */ 15 | 16 | const minFrameLen = 12 17 | 18 | const ( 19 | frameTypeIAX = 0x06 20 | subclassPoke = 0x1e // POKE message (similar to PING but doesn't require active connection) 21 | subclassPong = 0x03 // PONG 22 | subclassAck = 0x04 // ACK 23 | ) 24 | 25 | var frameHeaderIAX2Full = []byte{0x80, 0x0} 26 | 27 | func GetPokeFramePacket() []byte { 28 | // transforming golang structure to packed in-memory struct is pretty hard 29 | // so we will just construct the required packet manually 30 | var sCallNo = frameHeaderIAX2Full 31 | var dCallNo = []byte{0x0, 0x0} 32 | var ts = []byte{0x0, 0x0, 0x0, 0x0} 33 | var oSeqNo byte 34 | var iSeqNo byte 35 | var frameType byte = frameTypeIAX 36 | var cSub byte = subclassPoke 37 | 38 | var packet []byte 39 | packet = append(packet, sCallNo...) 40 | packet = append(packet, dCallNo...) 41 | packet = append(packet, ts...) 42 | packet = append(packet, oSeqNo, iSeqNo, frameType, cSub) 43 | 44 | return packet 45 | } 46 | 47 | func GetAckFramePacket() []byte { 48 | result := GetPokeFramePacket() 49 | result[11] = subclassAck 50 | return result 51 | } 52 | 53 | func IsPongResponse(frameBytes []byte) bool { 54 | if len(frameBytes) < minFrameLen { 55 | return false 56 | } 57 | 58 | var frameType = frameBytes[10] 59 | var cSub = frameBytes[11] 60 | 61 | return frameType == frameTypeIAX && cSub == subclassPong 62 | } 63 | -------------------------------------------------------------------------------- /synology-spk/create_spk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "$1" ] 4 | then 5 | echo "Usage: $0 VERSION" 6 | exit 7 | fi 8 | 9 | BUILD_PATH="github.com/cloudradar-monitoring/frontman/cmd/frontman/..." 10 | LD_FLAGS="-s -w -X main.version=$1" 11 | 12 | # ARMv7 13 | sed -i.bak "s/{PKG_VERSION}/$1/g" 2_create_project/INFO 14 | rm 2_create_project/INFO.bak 15 | sed -i.bak "s/{PKG_ARCH}/noarch/g" 2_create_project/INFO 16 | rm 2_create_project/INFO.bak 17 | 18 | CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="$LD_FLAGS" $BUILD_PATH 19 | mv -f frontman 1_create_package/frontman 20 | 21 | cd 1_create_package 22 | tar cvfz package.tgz * 23 | mv package.tgz ../2_create_project/ 24 | cd ../2_create_project/ 25 | tar cvfz frontman.spk * 26 | mv frontman.spk ../frontman-armv7.spk 27 | rm -f package.tgz 28 | cd .. 29 | 30 | git checkout 2_create_project/INFO 31 | 32 | # IMPORTANT: CGO_ENABLED=0 is used to force binaries to be statically linked 33 | 34 | # ARMv8 35 | sed -i.bak "s/{PKG_VERSION}/$1/g" 2_create_project/INFO 36 | rm 2_create_project/INFO.bak 37 | sed -i.bak "s/{PKG_ARCH}/noarch/g" 2_create_project/INFO 38 | rm 2_create_project/INFO.bak 39 | 40 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$LD_FLAGS" $BUILD_PATH 41 | mv -f frontman 1_create_package/frontman 42 | 43 | cd 1_create_package 44 | tar cvfz package.tgz * 45 | mv package.tgz ../2_create_project/ 46 | cd ../2_create_project/ 47 | tar cvfz frontman.spk * 48 | mv frontman.spk ../frontman-armv8.spk 49 | rm -f package.tgz 50 | cd .. 51 | 52 | git checkout 2_create_project/INFO 53 | 54 | 55 | # AMD64 56 | sed -i.bak "s/{PKG_VERSION}/$1/g" 2_create_project/INFO 57 | rm 2_create_project/INFO.bak 58 | sed -i.bak "s/{PKG_ARCH}/x86_64 cedarview bromolow broadwell/g" 2_create_project/INFO 59 | rm 2_create_project/INFO.bak 60 | 61 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$LD_FLAGS" $BUILD_PATH 62 | mv -f frontman 1_create_package/frontman 63 | 64 | cd 1_create_package 65 | tar cvfz package.tgz * 66 | mv package.tgz ../2_create_project/ 67 | cd ../2_create_project/ 68 | tar cvfz frontman.spk * 69 | mv frontman.spk ../frontman-amd64.spk 70 | rm -f package.tgz 71 | cd .. 72 | 73 | git checkout 2_create_project/INFO 74 | -------------------------------------------------------------------------------- /borrowed.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "net" 7 | "os" 8 | "os/exec" 9 | "runtime" 10 | "strings" 11 | "time" 12 | 13 | "github.com/shirou/gopsutil/host" 14 | ) 15 | 16 | type Invoker interface { 17 | Command(string, ...string) ([]byte, error) 18 | CommandWithContext(context.Context, string, ...string) ([]byte, error) 19 | } 20 | 21 | type Invoke struct{} 22 | 23 | func (i Invoke) Command(name string, arg ...string) ([]byte, error) { 24 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 25 | defer cancel() 26 | return i.CommandWithContext(ctx, name, arg...) 27 | } 28 | 29 | func (i Invoke) CommandWithContext(ctx context.Context, name string, arg ...string) ([]byte, error) { 30 | cmd := exec.CommandContext(ctx, name, arg...) 31 | 32 | var buf bytes.Buffer 33 | cmd.Stdout = &buf 34 | cmd.Stderr = &buf 35 | 36 | if err := cmd.Start(); err != nil { 37 | return buf.Bytes(), err 38 | } 39 | 40 | if err := cmd.Wait(); err != nil { 41 | return buf.Bytes(), err 42 | } 43 | 44 | return buf.Bytes(), nil 45 | } 46 | 47 | var invoke Invoker = Invoke{} 48 | 49 | func Uname() (string, error) { 50 | if runtime.GOOS == "windows" { 51 | info, err := host.Info() 52 | 53 | if err != nil { 54 | return "", err 55 | } 56 | return info.Platform + " " + info.PlatformVersion + " " + info.PlatformFamily, nil 57 | } 58 | uname, err := exec.LookPath("uname") 59 | if err != nil { 60 | return "", err 61 | } 62 | b, err := invoke.Command(uname, "-a") 63 | return strings.TrimSpace(string(b)), err 64 | } 65 | 66 | func getFQDN() string { 67 | hostname, err := os.Hostname() 68 | if err != nil { 69 | return "unknown" 70 | } 71 | 72 | addrs, err := net.LookupIP(hostname) 73 | if err != nil { 74 | return hostname 75 | } 76 | 77 | for _, addr := range addrs { 78 | if ipv4 := addr.To4(); ipv4 != nil { 79 | ip, err := ipv4.MarshalText() 80 | if err != nil { 81 | return hostname 82 | } 83 | hosts, err := net.LookupAddr(string(ip)) 84 | if err != nil || len(hosts) == 0 { 85 | return hostname 86 | } 87 | fqdn := hosts[0] 88 | return strings.TrimSuffix(fqdn, ".") 89 | } 90 | } 91 | return hostname 92 | } 93 | -------------------------------------------------------------------------------- /ssl_test.go: -------------------------------------------------------------------------------- 1 | // +build !quick_tests 2 | 3 | package frontman 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestFrontman_runSSLCheck(t *testing.T) { 12 | badSSL := []string{ 13 | "expired.badssl.com", 14 | "wrong.host.badssl.com", 15 | "self-signed.badssl.com", 16 | "untrusted-root.badssl.com", 17 | 18 | "sha1-intermediate.badssl.com", 19 | 20 | // cipher suite 21 | "rc4-md5.badssl.com", 22 | "rc4.badssl.com", 23 | "null.badssl.com", 24 | 25 | // key exchange 26 | "dh480.badssl.com", 27 | "dh512.badssl.com", 28 | "dh1024.badssl.com", 29 | "dh2048.badssl.com", 30 | "dh-small-subgroup.badssl.com", 31 | "dh-composite.badssl.com", 32 | 33 | // certificate transparency 34 | "invalid-expected-sct.badssl.com", 35 | 36 | // upgrade 37 | "subdomain.preloaded-hsts.badssl.com", 38 | 39 | // known bad 40 | "Superfish.badssl.com", 41 | "eDellRoot.badssl.com", 42 | "DSDTestProvider.badssl.com", 43 | "preact-cli.badssl.com", 44 | "webpack-dev-server.badssl.com", 45 | 46 | // chrome tests 47 | "captive-portal.badssl.com", 48 | "mitm-software.badssl.com", 49 | 50 | // defunct 51 | "sha1-2017.badssl.com", 52 | } 53 | 54 | goodSSL := []string{ 55 | "badssl.com", 56 | "sha256.badssl.com", 57 | "sha384.badssl.com", 58 | "sha512.badssl.com", 59 | 60 | "1000-sans.badssl.com", 61 | "ecc256.badssl.com", 62 | "ecc384.badssl.com", 63 | "rsa2048.badssl.com", 64 | "rsa4096.badssl.com", 65 | "extended-validation.badssl.com", 66 | "client.badssl.com", 67 | "mozilla-modern.badssl.com", 68 | 69 | "hsts.badssl.com", 70 | "upgrade.badssl.com", 71 | "preloaded-hsts.badssl.com", 72 | "https-everywhere.badssl.com", 73 | 74 | "long-extended-subdomain-name-containing-many-letters-and-dashes.badssl.com", 75 | "longextendedsubdomainnamewithoutdashesinordertotestwordwrapping.badssl.com", 76 | } 77 | 78 | if testing.Short() { 79 | badSSL = badSSL[:2] 80 | goodSSL = goodSSL[:2] 81 | } 82 | 83 | cfg := NewConfig() 84 | fm := helperCreateFrontman(t, cfg) 85 | 86 | for _, badSSLHost := range badSSL { 87 | _, err := fm.runSSLCheck(badSSLHost, 443, "https") 88 | assert.Error(t, err, badSSLHost) 89 | } 90 | 91 | for _, goodSSLHost := range goodSSL { 92 | _, err := fm.runSSLCheck(goodSSLHost, 443, "https") 93 | assert.NoError(t, err, goodSSLHost) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /pkg-scripts/postinstall-rpm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | CONFIG_PATH=/etc/frontman/frontman.conf 4 | 5 | # give frontman icmp ping rights 6 | if which setcap&>/dev/null;then 7 | setcap cap_net_raw=+ep /usr/bin/frontman 8 | fi 9 | 10 | # Install the first time: 1 11 | # Upgrade: 2 or higher (depending on the number of versions installed) 12 | versionsCount=$1 13 | 14 | # install selinux policy if SELinux is installed 15 | sestatus|grep -q "SELinux status:.*enabled" 16 | if [ $? -eq 0 ]; then 17 | if which checkmodule &>/dev/null; then 18 | echo "Installing SELinux policy for frontman" 19 | checkmodule -M -m -o frontman.mod /etc/frontman/frontman.tt 20 | semodule_package -o frontman.pp -m frontman.mod 21 | semodule -i frontman.pp 22 | else 23 | echo "### WARNING! ###" 24 | echo "Command 'checkmodule' missing. Please install package 'checkpolicy'." 25 | echo "If installed, run '/etc/frontman/se_linux_policy_install.sh'." 26 | fi 27 | fi 28 | 29 | if [ ${versionsCount} = 1 ]; then # fresh install 30 | /usr/bin/frontman -y -s frontman -c ${CONFIG_PATH} 31 | else # package update 32 | serviceStatus=`/usr/bin/frontman -y -service_status -c ${CONFIG_PATH}` 33 | echo "current service status: $serviceStatus." 34 | 35 | case "$serviceStatus" in 36 | unknown|failed) 37 | echo "trying to repair service..." 38 | /usr/bin/frontman -u || true 39 | /usr/bin/frontman -y -s frontman -c ${CONFIG_PATH} 40 | ;; 41 | 42 | running|stopped) 43 | # try to upgrade service unit config 44 | 45 | if [ "$serviceStatus" = running ]; then 46 | echo "stopping service..." 47 | /usr/bin/frontman -service_stop || true 48 | fi 49 | 50 | echo "upgrading service unit... " 51 | /usr/bin/frontman -y -s frontman -service_upgrade -c ${CONFIG_PATH} 52 | 53 | # restart only if it was active before 54 | if [ "$serviceStatus" = running ]; then 55 | echo "starting service... " 56 | /usr/bin/frontman -y -service_start -c ${CONFIG_PATH} 57 | fi 58 | ;; 59 | 60 | *) 61 | echo "unknown service status. Exiting..." 62 | exit 1 63 | ;; 64 | esac 65 | fi 66 | 67 | /usr/bin/frontman -t || true 68 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/scripts/start-stop-status: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # with WIZARD_FILES select log file or not 4 | # 5 | 6 | # Package 7 | PACKAGE="frontman" 8 | DNAME="Frontman" 9 | 10 | # Others 11 | INSTALL_DIR="/usr/local/${PACKAGE}" 12 | DIR_frontman="${INSTALL_DIR}/frontman" 13 | frontman="${DIR_frontman}/frontman" 14 | PID_FILE="${DIR_frontman}/frontman.pid" 15 | LOG_FILE="${DIR_frontman}/frontman.log" 16 | 17 | FILE_CREATE_LOG="${DIR_frontman}/wizard_create_log" 18 | 19 | export HOME=${DIR_frontman} 20 | #export PATH=$PATH:~/opt/bin # to Git. Not necessary with Git Server (Synology) 21 | export USER=frontman 22 | export USERNAME=frontman 23 | 24 | start_daemon () 25 | { 26 | cd ${DIR_frontman} 27 | if [ -e ${FILE_CREATE_LOG} ]; then 28 | ${frontman} > ${LOG_FILE} 2>&1 & 29 | else 30 | ${frontman} > /dev/null 2>&1 & 31 | fi 32 | echo $! > ${PID_FILE} 33 | } 34 | 35 | stop_daemon () 36 | { 37 | kill `cat ${PID_FILE}` 38 | wait_for_status 1 20 || kill -9 `cat ${PID_FILE}` 39 | rm -f ${PID_FILE} 40 | } 41 | 42 | daemon_status () 43 | { 44 | if [ -f ${PID_FILE} ] && kill -0 `cat ${PID_FILE}` > /dev/null 2>&1; then 45 | return 46 | fi 47 | rm -f ${PID_FILE} 48 | return 1 49 | } 50 | 51 | wait_for_status () 52 | { 53 | counter=$2 54 | while [ ${counter} -gt 0 ]; do 55 | daemon_status 56 | [ $? -eq $1 ] && return 57 | let counter=counter-1 58 | sleep 1 59 | done 60 | return 1 61 | } 62 | 63 | 64 | case $1 in 65 | start) 66 | if daemon_status; then 67 | echo ${DNAME} is already running 68 | else 69 | echo Starting ${DNAME} ... 70 | start_daemon 71 | fi 72 | ;; 73 | stop) 74 | if daemon_status; then 75 | echo Stopping ${DNAME} ... 76 | stop_daemon 77 | else 78 | echo ${DNAME} is not running 79 | fi 80 | ;; 81 | restart) 82 | stop_daemon 83 | start_daemon 84 | ;; 85 | status) 86 | if daemon_status; then 87 | echo ${DNAME} is running 88 | exit 0 89 | else 90 | echo ${DNAME} is not running 91 | exit 1 92 | fi 93 | ;; 94 | log) 95 | echo ${LOG_FILE} 96 | ;; 97 | *) 98 | exit 1 99 | ;; 100 | esac 101 | -------------------------------------------------------------------------------- /pkg-scripts/msi-templates/LicenseAgreementDlg_HK.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | CostingComplete = 1 12 | "1"]]> 13 | LicenseAccepted = "1" 14 | 15 | 16 | 1 17 | 18 | 19 | 20 | 21 | {{if gt (.License | len) 0}} 22 | 23 | {{end}} 24 | 25 | 26 | 27 | 1 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /service.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strings" 7 | 8 | "github.com/kardianos/service" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func tryStartService(s service.Service) { 13 | logrus.Info("Starting service...") 14 | err := s.Start() 15 | if err != nil { 16 | logrus.WithError(err).Warningf("Frontman service(%s) startup failed", s.Platform()) 17 | } 18 | } 19 | 20 | func tryInstallService(s service.Service, assumeYesPtr *bool) { 21 | const maxAttempts = 3 22 | for attempt := 1; attempt <= maxAttempts; attempt++ { 23 | err := s.Install() 24 | // Check error case where the service already exists 25 | switch { 26 | case err != nil && strings.Contains(err.Error(), "already exists"): 27 | if attempt == maxAttempts { 28 | logrus.Fatalf("Giving up after %d attempts", maxAttempts) 29 | } 30 | 31 | var osSpecificNote string 32 | if runtime.GOOS == "windows" { 33 | osSpecificNote = " Windows Services Manager application must be closed before proceeding!" 34 | } 35 | 36 | fmt.Printf("Frontman service(%s) already installed: %s\n", s.Platform(), err.Error()) 37 | if *assumeYesPtr || AskForConfirmation("Do you want to overwrite it?"+osSpecificNote) { 38 | logrus.Info("Trying to override old service unit...") 39 | err = s.Stop() 40 | if err != nil { 41 | logrus.WithError(err).Warnln("Failed to stop the service") 42 | } 43 | 44 | // lets try to uninstall despite of this error 45 | err := s.Uninstall() 46 | if err != nil { 47 | logrus.WithError(err).Fatalln("Failed to uninstall the service") 48 | } 49 | } 50 | case err != nil: 51 | logrus.WithError(err).Fatalf("Frontman service(%s) installation failed", s.Platform()) 52 | default: 53 | logrus.Infof("Frontman service(%s) has been installed.", s.Platform()) 54 | return 55 | } 56 | } 57 | } 58 | 59 | func (fm *Frontman) tryUpgradeServiceUnit(s service.Service) { 60 | _, err := s.Status() 61 | if err == service.ErrNotInstalled { 62 | logrus.Error("Can't upgrade service: service is not installed") 63 | return 64 | } 65 | 66 | fm.configureServiceEnabledState(s) 67 | 68 | err = s.Stop() 69 | if err != nil { 70 | logrus.WithError(err).Warnln("Failed to stop the service") 71 | } 72 | 73 | err = s.Uninstall() 74 | if err != nil { 75 | logrus.WithError(err).Fatalln("Failed to uninstall the service") 76 | } 77 | 78 | err = s.Install() 79 | if err != nil { 80 | logrus.WithError(err).Fatalf("Frontman service(%s) unit upgrade failed", s.Platform()) 81 | } 82 | 83 | logrus.Infof("Frontman service(%s) unit has been upgraded.", s.Platform()) 84 | } 85 | -------------------------------------------------------------------------------- /service_notwindows.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package frontman 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "os/user" 10 | "path/filepath" 11 | "strconv" 12 | 13 | "github.com/kardianos/service" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | func updateServiceConfig(fm *Frontman, userName string) { 18 | u, err := user.Lookup(userName) 19 | if err != nil { 20 | logrus.WithFields(logrus.Fields{ 21 | "user": userName, 22 | }).WithError(err).Fatalln("Failed to find the user") 23 | } 24 | fm.serviceConfig.UserName = userName 25 | // we need to chown log file with user who will run service 26 | // because installer can be run under root so the log file will be also created under root 27 | err = chownFile(fm.Config.LogFile, u) 28 | if err != nil { 29 | logrus.WithFields(logrus.Fields{ 30 | "user": userName, 31 | }).WithError(err).Warnln("Failed to chown log file") 32 | } 33 | } 34 | 35 | func chownFile(filePath string, u *user.User) error { 36 | uid, err := strconv.Atoi(u.Uid) 37 | if err != nil { 38 | return fmt.Errorf("chown files: error converting UID(%s) to int", u.Uid) 39 | } 40 | 41 | gid, err := strconv.Atoi(u.Gid) 42 | if err != nil { 43 | return fmt.Errorf("chown files: error converting GID(%s) to int", u.Gid) 44 | } 45 | 46 | return os.Chown(filePath, uid, gid) 47 | } 48 | 49 | func (fm *Frontman) configureServiceEnabledState(s service.Service) { 50 | serviceMgrName := s.Platform() 51 | isServiceAlreadyEnabled := true 52 | if serviceMgrName == "linux-systemd" { 53 | isServiceAlreadyEnabled = checkIfSystemdServiceEnabled(fm.serviceConfig.Name) 54 | } 55 | if serviceMgrName == "unix-systemv" { 56 | isServiceAlreadyEnabled = checkIfSysvServiceEnabled(fm.serviceConfig.Name) 57 | } 58 | 59 | if fm.serviceConfig.Option == nil { 60 | fm.serviceConfig.Option = service.KeyValue{} 61 | } 62 | 63 | fm.serviceConfig.Option["Enabled"] = isServiceAlreadyEnabled 64 | } 65 | 66 | func checkIfSystemdServiceEnabled(serviceName string) bool { 67 | cmd := exec.Command("systemctl", "is-enabled", serviceName+".service") 68 | err := cmd.Run() 69 | return err == nil 70 | } 71 | 72 | func checkIfSysvServiceEnabled(serviceName string) bool { 73 | configPath := "/etc/init.d/" + serviceName 74 | runLevels := []string{"1", "2", "3", "4", "5"} 75 | 76 | // search config symlinks in each runlevel folder: 77 | for _, level := range runLevels { 78 | dirFiles, _ := filepath.Glob("/etc/rc" + level + ".d/*" + serviceName) 79 | for _, file := range dirFiles { 80 | linkSrc, _ := filepath.EvalSymlinks(file) 81 | if absLinkPath, _ := filepath.Abs(linkSrc); absLinkPath == configPath { 82 | return true 83 | } 84 | } 85 | } 86 | 87 | return false 88 | } 89 | -------------------------------------------------------------------------------- /servicecheck.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func (check ServiceCheck) uniqueID() string { 12 | return check.UUID 13 | } 14 | 15 | func (check ServiceCheck) run(fm *Frontman) (*Result, error) { 16 | 17 | res := &Result{ 18 | Node: fm.Config.NodeName, 19 | CheckType: "serviceCheck", 20 | CheckUUID: check.UUID, 21 | Check: check.Check, 22 | Timestamp: time.Now().Unix(), 23 | } 24 | 25 | if check.UUID == "" { 26 | return res, fmt.Errorf("missing checkUuid key") 27 | } 28 | if check.Check.Connect == "" { 29 | return res, fmt.Errorf("missing data.connect key") 30 | } 31 | 32 | var done = make(chan struct{}) 33 | var err error 34 | var results map[string]interface{} 35 | 36 | go func() { 37 | switch check.Check.Protocol { 38 | case ProtocolICMP: 39 | results, err = fm.runPing(check.Check.Connect) 40 | if err != nil { 41 | logrus.Debugf("serviceCheck: %s: %s", check.UUID, err.Error()) 42 | } 43 | case ProtocolTCP: 44 | port, _ := check.Check.Port.Int64() 45 | 46 | results, err = fm.runTCPCheck(check.Check.Connect, int(port), check.Check.Service) 47 | if err != nil { 48 | logrus.Debugf("serviceCheck: %s: %s", check.UUID, err.Error()) 49 | } 50 | case ProtocolUDP: 51 | port, _ := check.Check.Port.Int64() 52 | 53 | results, err = fm.runUDPCheck(check.Check.Connect, int(port), check.Check.Service) 54 | if err != nil { 55 | logrus.Debugf("serviceCheck: %s: %s", check.UUID, err.Error()) 56 | } 57 | case ProtocolSSL: 58 | port, _ := check.Check.Port.Int64() 59 | 60 | results, err = fm.runSSLCheck(check.Check.Connect, int(port), check.Check.Service) 61 | if err != nil { 62 | logrus.Debugf("serviceCheck: %s: %s", check.UUID, err.Error()) 63 | } 64 | case "": 65 | logrus.Info("serviceCheck: missing check.protocol") 66 | err = errors.New("Missing check.protocol") 67 | default: 68 | logrus.Errorf("serviceCheck: unknown check.protocol: '%s'", check.Check.Protocol) 69 | err = errors.New("Unknown check.protocol") 70 | } 71 | done <- struct{}{} 72 | }() 73 | 74 | // Warning: do not rely on serviceCheckEmergencyTimeout as it leak goroutines(until it will be finished) 75 | // instead use individual timeouts inside all checks 76 | select { 77 | case <-done: 78 | res.Measurements = results 79 | return res, err 80 | case <-time.After(serviceCheckEmergencyTimeout): 81 | logrus.Debugf("serviceCheck %s %s: %s got unexpected timeout after %.0fs", check.Check.Protocol, check.Check.Connect, check.UUID, serviceCheckEmergencyTimeout.Seconds()) 82 | return res, fmt.Errorf("got unexpected timeout") 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pkg/winui/winui_service.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package winui 4 | 5 | import ( 6 | "context" 7 | "strings" 8 | "time" 9 | 10 | "github.com/pkg/errors" 11 | "golang.org/x/sys/windows/svc" 12 | "golang.org/x/sys/windows/svc/mgr" 13 | ) 14 | 15 | func (ui *UI) reloadService() (err error) { 16 | ctx := context.Background() 17 | 18 | m, err2 := mgr.Connect() 19 | if err2 != nil { 20 | err = errors.New("Failed to connect to Windows Service Manager") 21 | return 22 | } 23 | defer m.Disconnect() 24 | 25 | s, err := m.OpenService("frontman") 26 | if err != nil { 27 | err = errors.New("Failed to find Frontman service") 28 | return 29 | } 30 | defer s.Close() 31 | 32 | ui.SaveButton.SetText("Stopping the service...") 33 | 34 | if err2 := stopService(ctx, s); err2 != nil { 35 | err = errors.New("Failed to stop Frontman service") 36 | return 37 | } 38 | 39 | ui.SaveButton.SetText("Starting the service...") 40 | if err2 := startService(ctx, s); err2 != nil { 41 | err = errors.New("Failed to start Frontman service") 42 | return 43 | } 44 | 45 | ui.StatusBar.SetText("Status: successfully connected to the Hub") 46 | ui.StatusBar.SetIcon(ui.SuccessIcon) 47 | return 48 | } 49 | 50 | func startService(ctx context.Context, s *mgr.Service) error { 51 | err := s.Start("is", "manual-started") 52 | if err != nil { 53 | err = errors.Wrap(err, "could not schedule a service to start") 54 | return err 55 | } 56 | 57 | return waitServiceState(ctx, s, svc.Running) 58 | } 59 | 60 | func stopService(ctx context.Context, s *mgr.Service) error { 61 | status, err := s.Control(svc.Stop) 62 | if err != nil { 63 | if strings.Contains(err.Error(), "has not been started") { 64 | return nil 65 | } 66 | err = errors.Wrap(err, "could not schedule a service to stop") 67 | return err 68 | } 69 | if status.State == svc.Stopped { 70 | return nil 71 | } 72 | return waitServiceState(ctx, s, svc.Stopped) 73 | } 74 | 75 | // waitServiceState checks the current state of a service and waits until it will match 76 | // the expectedState, or a context deadline appearing first. 77 | func waitServiceState(ctx context.Context, s *mgr.Service, expectedState svc.State) error { 78 | for { 79 | select { 80 | case <-ctx.Done(): 81 | if ctx.Err() == context.DeadlineExceeded { 82 | err := errors.Wrap(ctx.Err(), "timeout waiting for service to stop") 83 | return err 84 | } 85 | return nil 86 | default: 87 | currentStatus, err := s.Query() 88 | if err != nil { 89 | err := errors.Wrap(err, "could not retrieve service status") 90 | return err 91 | } 92 | if currentStatus.State == expectedState { 93 | return nil 94 | } 95 | time.Sleep(300 * time.Millisecond) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /hostinfo.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "strings" 9 | "time" 10 | 11 | "github.com/pkg/errors" 12 | "github.com/shirou/gopsutil/cpu" 13 | "github.com/shirou/gopsutil/host" 14 | "github.com/shirou/gopsutil/mem" 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | // HostInfoResults fetches information about the host itself which can be 19 | // send to the hub alongside measurements. 20 | func (fm *Frontman) HostInfoResults() (MeasurementsMap, error) { 21 | res := MeasurementsMap{} 22 | 23 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 24 | defer cancel() 25 | 26 | info, err := host.InfoWithContext(ctx) 27 | errs := []string{} 28 | 29 | if err != nil { 30 | if ctx.Err() == context.DeadlineExceeded { 31 | err = fmt.Errorf("timeout exceeded") 32 | } 33 | 34 | logrus.Errorf("[SYSTEM] Failed to read host info: %s", err.Error()) 35 | errs = append(errs, err.Error()) 36 | } 37 | 38 | for _, field := range fm.Config.HostInfo { 39 | switch strings.ToLower(field) { 40 | case "os_kernel": 41 | if info != nil { 42 | res[field] = info.OS 43 | } else { 44 | res[field] = nil 45 | } 46 | case "os_family": 47 | if info != nil { 48 | res[field] = info.PlatformFamily 49 | } else { 50 | res[field] = nil 51 | } 52 | case "uname": 53 | uname, err := Uname() 54 | if err != nil { 55 | logrus.Errorf("[SYSTEM] Failed to read host uname: %s", err.Error()) 56 | errs = append(errs, err.Error()) 57 | res[field] = nil 58 | } else { 59 | res[field] = uname 60 | } 61 | case "fqdn": 62 | res[field] = getFQDN() 63 | case "cpu_model": 64 | cpuInfo, err := cpu.Info() 65 | if err != nil { 66 | logrus.Errorf("[SYSTEM] Failed to read cpu info: %s", err.Error()) 67 | errs = append(errs, err.Error()) 68 | res[field] = nil 69 | continue 70 | } 71 | res[field] = cpuInfo[0].ModelName 72 | case "os_arch": 73 | res[field] = runtime.GOARCH 74 | case "memory_total_b": 75 | memStat, err := mem.VirtualMemory() 76 | if err != nil { 77 | logrus.Errorf("[SYSTEM] Failed to read mem info: %s", err.Error()) 78 | errs = append(errs, err.Error()) 79 | res[field] = nil 80 | continue 81 | } 82 | res[field] = memStat.Total 83 | case "hostname": 84 | name, err := os.Hostname() 85 | if err != nil { 86 | logrus.Errorf("[SYSTEM] Failed to read hostname: %s", err.Error()) 87 | errs = append(errs, err.Error()) 88 | res[field] = nil 89 | continue 90 | } 91 | res[field] = name 92 | } 93 | } 94 | 95 | if len(errs) == 0 { 96 | return res, nil 97 | } 98 | 99 | return res, errors.New("SYSTEM: " + strings.Join(errs, "; ")) 100 | } 101 | -------------------------------------------------------------------------------- /pkg/winui/winui_logview.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Walk Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build windows 6 | 7 | package winui 8 | 9 | import ( 10 | "errors" 11 | "syscall" 12 | "unsafe" 13 | 14 | "github.com/lxn/walk" 15 | "github.com/lxn/win" 16 | ) 17 | 18 | type LogView struct { 19 | walk.WidgetBase 20 | logChan chan string 21 | } 22 | 23 | const ( 24 | TEM_APPENDTEXT = win.WM_USER + 6 25 | ) 26 | 27 | func NewLogView(parent walk.Container) (*LogView, error) { 28 | lc := make(chan string, 1024) 29 | lv := &LogView{logChan: lc} 30 | 31 | if err := walk.InitWidget( 32 | lv, 33 | parent, 34 | "EDIT", 35 | win.WS_TABSTOP|win.WS_VISIBLE|win.WS_VSCROLL|win.ES_MULTILINE|win.ES_WANTRETURN, 36 | win.WS_EX_CLIENTEDGE); err != nil { 37 | return nil, err 38 | } 39 | lv.setReadOnly(true) 40 | lv.SendMessage(win.EM_SETLIMITTEXT, 4294967295, 0) 41 | return lv, nil 42 | } 43 | 44 | func (*LogView) LayoutFlags() walk.LayoutFlags { 45 | return walk.ShrinkableHorz | walk.ShrinkableVert | walk.GrowableHorz | walk.GrowableVert | walk.GreedyHorz | walk.GreedyVert 46 | } 47 | 48 | func (*LogView) MinSizeHint() walk.Size { 49 | return walk.Size{Width: 20, Height: 12} 50 | } 51 | 52 | func (*LogView) SizeHint() walk.Size { 53 | return walk.Size{Width: 100, Height: 100} 54 | } 55 | 56 | func (lv *LogView) setTextSelection(start, end int) { 57 | lv.SendMessage(win.EM_SETSEL, uintptr(start), uintptr(end)) 58 | } 59 | 60 | func (lv *LogView) textLength() int { 61 | return int(lv.SendMessage(0x000E, uintptr(0), uintptr(0))) 62 | } 63 | 64 | func (lv *LogView) AppendText(value string) { 65 | textLength := lv.textLength() 66 | lv.setTextSelection(textLength, textLength) 67 | lv.SendMessage(win.EM_REPLACESEL, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(value)))) 68 | } 69 | 70 | func (lv *LogView) setReadOnly(readOnly bool) error { 71 | if 0 == lv.SendMessage(win.EM_SETREADONLY, uintptr(win.BoolToBOOL(readOnly)), 0) { 72 | return errors.New("fail to call EM_SETREADONLY") 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func (lv *LogView) PostAppendText(value string) { 79 | lv.logChan <- value 80 | win.PostMessage(lv.Handle(), TEM_APPENDTEXT, 0, 0) 81 | } 82 | 83 | func (lv *LogView) Write(p []byte) (int, error) { 84 | lv.PostAppendText(string(p) + "\r\n") 85 | return len(p), nil 86 | } 87 | 88 | func (lv *LogView) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr { 89 | switch msg { 90 | case win.WM_GETDLGCODE: 91 | if wParam == win.VK_RETURN { 92 | return win.DLGC_WANTALLKEYS 93 | } 94 | 95 | return win.DLGC_HASSETSEL | win.DLGC_WANTARROWS | win.DLGC_WANTCHARS 96 | case TEM_APPENDTEXT: 97 | select { 98 | case value := <-lv.logChan: 99 | lv.AppendText(value) 100 | default: 101 | return 0 102 | } 103 | } 104 | 105 | return lv.WidgetBase.WndProc(hwnd, msg, wParam, lParam) 106 | } 107 | -------------------------------------------------------------------------------- /ssl.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "fmt" 8 | "math" 9 | "net" 10 | "strings" 11 | "time" 12 | 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | const timeoutPortLookup = time.Second * 3 17 | 18 | func certName(cert *x509.Certificate) string { 19 | return fmt.Sprintf("'%s' issued by %s", cert.Subject.CommonName, cert.Issuer.CommonName) 20 | } 21 | 22 | func (fm *Frontman) runSSLCheck(hostname string, port int, service string) (m MeasurementsMap, err error) { 23 | service = strings.ToLower(service) 24 | 25 | if net.ParseIP(hostname) != nil { 26 | hostname = "" 27 | } 28 | 29 | if port == 0 { 30 | ctx, cancel := context.WithTimeout(context.Background(), timeoutPortLookup) 31 | defer cancel() 32 | 33 | if p, exists := defaultPortByService[service]; exists { 34 | port = p 35 | } else if p, lerr := net.DefaultResolver.LookupPort(ctx, "tcp", service); p > 0 { 36 | port = p 37 | } else if lerr != nil { 38 | err = fmt.Errorf("failed to auto-determine port for '%s': %s", service, lerr.Error()) 39 | return 40 | } 41 | } 42 | 43 | prefix := fmt.Sprintf("net.tcp.ssl.%d.", port) 44 | 45 | m = MeasurementsMap{ 46 | prefix + "success": 0, 47 | } 48 | 49 | addr := fmt.Sprintf("%s:%d", hostname, port) 50 | dialer := net.Dialer{Timeout: secToDuration(fm.Config.NetTCPTimeout)} 51 | connection, err := tls.DialWithDialer( 52 | &dialer, 53 | "tcp", 54 | addr, 55 | &tls.Config{ServerName: hostname}, 56 | ) 57 | if err != nil { 58 | logrus.Debugf("serviceCheck: SSL check %s for '%s' failed: %s", addr, hostname, err.Error()) 59 | if strings.HasPrefix(err.Error(), "tls:") { 60 | err = fmt.Errorf("service doesn't support SSL") 61 | } else { 62 | err = fmt.Errorf(strings.TrimPrefix(err.Error(), "x509: ")) 63 | } 64 | return 65 | } 66 | 67 | defer connection.Close() 68 | 69 | remainingValidity, firstCertToExpire := findCertRemainingValidity(connection.ConnectionState().VerifiedChains) 70 | m[prefix+"expiryDaysRemaining"] = remainingValidity 71 | 72 | if remainingValidity <= float64(fm.Config.SSLCertExpiryThreshold) { 73 | err = fmt.Errorf("certificate will expire soon: %s", certName(firstCertToExpire)) 74 | return 75 | } 76 | 77 | m[prefix+"success"] = 1 78 | return 79 | } 80 | 81 | func findCertRemainingValidity(certChains [][]*x509.Certificate) (float64, *x509.Certificate) { 82 | var remainingValidity float64 83 | var firstToExpire *x509.Certificate 84 | 85 | // find chain with max remaining validity 86 | for _, chain := range certChains { 87 | chainRemainingValidity, c := findChainRemainingValidity(chain) 88 | if chainRemainingValidity > remainingValidity { 89 | remainingValidity = chainRemainingValidity 90 | firstToExpire = c 91 | } 92 | } 93 | return remainingValidity, firstToExpire 94 | } 95 | 96 | func findChainRemainingValidity(chain []*x509.Certificate) (float64, *x509.Certificate) { 97 | var min = math.MaxFloat64 98 | var firstToExpire *x509.Certificate 99 | 100 | // find cert that will expire first 101 | for _, cert := range chain { 102 | remainingValidity := time.Until(cert.NotAfter).Hours() / 24 103 | if remainingValidity < min { 104 | min = remainingValidity 105 | firstToExpire = cert 106 | } 107 | } 108 | return min, firstToExpire 109 | } 110 | -------------------------------------------------------------------------------- /.circleci/publish-packages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xe 4 | 5 | ssh_ci() { 6 | ssh -p 24480 -oStrictHostKeyChecking=no ci@repo.cloudradar.io "$@" 7 | } 8 | 9 | ssh_cr() { 10 | ssh -p 24480 -oStrictHostKeyChecking=no cr@repo.cloudradar.io "$@" 11 | } 12 | 13 | github_upload() { 14 | github-release upload --user cloudradar-monitoring --repo frontman --tag ${CIRCLE_TAG} "$@" 15 | } 16 | 17 | if [ -z ${RELEASE_MODE} ]; then 18 | echo "RELEASE_MODE env variable is empty" 19 | exit 1 20 | fi 21 | 22 | PROJECT_NAME=github.com/cloudradar-monitoring/frontman 23 | PROJECT_DIR=/go/src/${PROJECT_NAME} 24 | WORK_DIR=/home/ci/buffer/${CIRCLE_BUILD_NUM} 25 | SIGN_MSI=true 26 | 27 | # create build dir structure 28 | ssh_ci mkdir -p ${WORK_DIR}/deb 29 | ssh_ci mkdir -p ${WORK_DIR}/rpm 30 | ssh_ci mkdir -p ${WORK_DIR}/msi 31 | 32 | # send packages 33 | rsync -e 'ssh -oStrictHostKeyChecking=no -p 24480' ${PROJECT_DIR}/dist/*.deb --exclude "*_armv6.deb" ci@repo.cloudradar.io:${WORK_DIR}/deb/ 34 | rsync -e 'ssh -oStrictHostKeyChecking=no -p 24480' ${PROJECT_DIR}/dist/*.rpm ci@repo.cloudradar.io:${WORK_DIR}/rpm/ 35 | rsync -e 'ssh -oStrictHostKeyChecking=no -p 24480' -a --files-from=${PROJECT_DIR}/.circleci/rsync-msi-build.list ${PROJECT_DIR} ci@repo.cloudradar.io:${WORK_DIR}/msi/ 36 | 37 | # publish to DEB and RPM repo 38 | if [ ${RELEASE_MODE} = "stable" ]; then 39 | ssh_cr /home/cr/work/aptly/update_repo.sh ${WORK_DIR}/deb ${CIRCLE_TAG} 40 | ssh_cr /home/cr/work/rpm/update_repo_frontman.sh ${WORK_DIR}/rpm ${CIRCLE_TAG} 41 | fi 42 | 43 | if [ ${SIGN_MSI} = true ]; then 44 | 45 | # trigger MSI build and sign 46 | ssh_cr /home/cr/work/msi/frontman_build_and_sign_msi.sh ${WORK_DIR}/msi ${CIRCLE_BUILD_NUM} ${CIRCLE_TAG} 47 | 48 | # copy signed MSI back 49 | scp -P 24480 -oStrictHostKeyChecking=no ci@repo.cloudradar.io:${WORK_DIR}/msi/frontman_64.msi ${PROJECT_DIR}/dist/frontman_64.msi 50 | 51 | # scan signed MSI 52 | go get github.com/cloudradar-monitoring/virustotal-scan 53 | virustotal-scan --verbose --ignore Cylance,Jiangmin,Ikarus,MaxSecure,Microsoft --apikey ${VIRUSTOTAL_TOKEN} --file ${PROJECT_DIR}/dist/frontman_64.msi 54 | 55 | github_upload --name "frontman_${CIRCLE_TAG}_Windows_x86_64.msi" --file "${PROJECT_DIR}/dist/frontman_64.msi" 56 | fi 57 | 58 | # publish built files to Github 59 | github_upload --name "frontman_${CIRCLE_TAG}_synology_amd64.spk" --file "${PROJECT_DIR}/synology-spk/frontman-amd64.spk" 60 | github_upload --name "frontman_${CIRCLE_TAG}_synology_armv7.spk" --file "${PROJECT_DIR}/synology-spk/frontman-armv7.spk" 61 | github_upload --name "frontman_${CIRCLE_TAG}_synology_armv8.spk" --file "${PROJECT_DIR}/synology-spk/frontman-armv8.spk" 62 | 63 | # fetch release changelog so we can preserve it when releasing 64 | CHANGELOGRAW=$(curl -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/cloudradar-monitoring/frontman/releases | jq ".[0].body") 65 | 66 | # update release status 67 | PRERELEASE="--pre-release" 68 | if [ ${RELEASE_MODE} = "stable" ]; then 69 | PRERELEASE= 70 | fi 71 | echo -e ${CHANGELOGRAW} | sed -e 's/^"//' -e 's/"$//' | github-release edit --user cloudradar-monitoring --repo frontman --tag ${CIRCLE_TAG} ${PRERELEASE} --description - 72 | 73 | if [ ${SIGN_MSI} = true ]; then 74 | # update MSI repo 75 | ssh_cr /home/cr/work/msi/frontman_publish.sh ${WORK_DIR}/msi ${CIRCLE_TAG} ${RELEASE_MODE} 76 | fi 77 | 78 | # remove work dir 79 | ssh_ci rm -rf ${WORK_DIR} 80 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestPingHandler(t *testing.T) { 14 | req, err := http.NewRequest("GET", "/ping", nil) 15 | assert.Equal(t, nil, err) 16 | 17 | rr := httptest.NewRecorder() 18 | handler := http.HandlerFunc(pingHandler) 19 | handler.ServeHTTP(rr, req) 20 | assert.Equal(t, http.StatusOK, rr.Code) 21 | 22 | expected := `{"alive": true}` 23 | assert.Equal(t, expected, rr.Body.String()) 24 | } 25 | 26 | func TestHttpCheckHandler(t *testing.T) { 27 | checks := `{ 28 | "webChecks": [{ 29 | "checkUUID": "web_head_status_matched", 30 | "check": { "url": "https://www.google.com", "method": "head", "expectedHttpStatus": 200} 31 | }] 32 | }` 33 | 34 | cfg := NewConfig() 35 | fm := helperCreateFrontman(t, cfg) 36 | 37 | reader := strings.NewReader(checks) 38 | req, err := http.NewRequest("POST", "/check", reader) 39 | assert.Equal(t, nil, err) 40 | req.Header.Set("Content-Type", "application/json") 41 | 42 | rr := httptest.NewRecorder() 43 | handler := http.HandlerFunc(fm.checkHandler) 44 | handler.ServeHTTP(rr, req) 45 | assert.Equal(t, http.StatusOK, rr.Code) 46 | 47 | data := rr.Body.Bytes() 48 | assert.Equal(t, true, len(data) > 2) 49 | 50 | var f interface{} 51 | err = json.Unmarshal(data, &f) 52 | assert.Equal(t, nil, err) 53 | 54 | dec1 := f.([]interface{}) 55 | dec := dec1[0].(map[string]interface{}) 56 | measurements := dec["measurements"].(map[string]interface{}) 57 | 58 | assert.Equal(t, nil, dec["message"]) 59 | assert.Equal(t, "webCheck", dec["checkType"]) 60 | assert.Equal(t, "web_head_status_matched", dec["checkUuid"]) 61 | assert.Equal(t, 200., measurements["http.head.httpStatusCode"]) 62 | assert.Equal(t, 1., measurements["http.head.success"]) 63 | assert.Equal(t, map[string]interface{}{ 64 | "dontFollowRedirects": false, 65 | "expectedHttpStatus": 200., 66 | "method": "head", 67 | "searchHtmlSource": false, 68 | "url": "https://www.google.com", 69 | }, dec["check"]) 70 | } 71 | 72 | func TestHttpCheckInvalidHost(t *testing.T) { 73 | checks := `{ 74 | "webChecks": [{ 75 | "checkUUID": "webcheck_broken", 76 | "check": { "url": "http://notfound.site.com/", "method": "get", "expectedHttpStatus": 200} 77 | }] 78 | }` 79 | 80 | cfg := NewConfig() 81 | fm := helperCreateFrontman(t, cfg) 82 | 83 | reader := strings.NewReader(checks) 84 | req, err := http.NewRequest("POST", "/check", reader) 85 | assert.Equal(t, nil, err) 86 | req.Header.Set("Content-Type", "application/json") 87 | 88 | rr := httptest.NewRecorder() 89 | handler := http.HandlerFunc(fm.checkHandler) 90 | handler.ServeHTTP(rr, req) 91 | assert.Equal(t, http.StatusOK, rr.Code) 92 | 93 | data := rr.Body.Bytes() 94 | assert.Equal(t, true, len(data) > 2) 95 | 96 | var f interface{} 97 | err = json.Unmarshal(data, &f) 98 | assert.Equal(t, nil, err) 99 | 100 | dec1 := f.([]interface{}) 101 | dec := dec1[0].(map[string]interface{}) 102 | 103 | assert.NotEqual(t, nil, dec["measurements"]) 104 | measurements := dec["measurements"].(map[string]interface{}) 105 | 106 | assert.Equal(t, "webCheck", dec["checkType"]) 107 | assert.Equal(t, "webcheck_broken", dec["checkUuid"]) 108 | assert.Equal(t, 0., measurements["http.get.success"]) 109 | } 110 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - CGO_ENABLED=0 3 | - PROJECT=github.com/cloudradar-monitoring/frontman 4 | builds: 5 | - main: ./cmd/frontman 6 | binary: frontman 7 | goos: 8 | - windows 9 | - darwin 10 | - linux 11 | goarch: 12 | - 386 13 | - amd64 14 | - arm 15 | - arm64 16 | - mipsle 17 | goarm: 18 | - 5 19 | - 6 20 | - 7 21 | gomips: 22 | - hardfloat 23 | - softfloat 24 | # List of combinations of GOOS + GOARCH + GOARM to ignore. 25 | ignore: 26 | - goos: darwin 27 | goarch: 386 28 | - goos: windows 29 | goarch: 386 30 | - goos: windows 31 | goarch: arm 32 | - goos: darwin 33 | goarch: arm 34 | - goos: windows 35 | goarch: arm64 36 | - goos: darwin 37 | goarch: arm64 38 | - goos: darwin 39 | goarch: mipsle 40 | - goos: windows 41 | goarch: mipsle 42 | ldflags: 43 | - "-s -w -X {{.Env.PROJECT}}.Version={{.Version}} -X \"{{.Env.PROJECT}}.SelfUpdatesFeedURL={{.Env.SELF_UPDATES_FEED_URL}}\"" 44 | archives: 45 | - 46 | files: 47 | - README.md 48 | - example.json 49 | - example.config.toml 50 | replacements: 51 | darwin: Darwin 52 | linux: Linux 53 | windows: Windows 54 | 386: i386 55 | amd64: x86_64 56 | format_overrides: 57 | - goos: windows 58 | format: zip 59 | checksum: 60 | name_template: 'checksums.txt' 61 | snapshot: 62 | name_template: "{{ .Tag }}" 63 | changelog: 64 | sort: asc 65 | filters: 66 | exclude: 67 | - '^docs:' 68 | - '^test:' 69 | nfpms: 70 | - 71 | file_name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" 72 | 73 | vendor: cloudradar GmbH 74 | maintainer: CloudRadar GmbH 75 | homepage: https://cloudradar.io 76 | description: Monitoring proxy for agentless monitoring of subnets 77 | license: MIT 78 | 79 | # Formats to be generated. 80 | formats: 81 | - deb 82 | - rpm 83 | 84 | recommends: 85 | - ca-certificates 86 | - procps 87 | 88 | # Override default /usr/local/bin destination for binaries 89 | bindir: /usr/bin 90 | 91 | # Empty folders that should be created and managed by the packager 92 | # implementation. 93 | empty_folders: 94 | - /var/log/frontman 95 | - /etc/frontman 96 | - /usr/share/frontman 97 | 98 | # Put example.json 99 | files: 100 | "example.json": "/usr/share/doc/frontman/example.json" 101 | "example.config.toml": "/etc/frontman/example.config.toml" 102 | "cacert.pem": "/etc/frontman/cacert.pem" 103 | "pkg-scripts/frontman.tt": "/etc/frontman/frontman.tt" 104 | "pkg-scripts/se_linux_policy_install.sh": "/etc/frontman/se_linux_policy_install.sh" 105 | 106 | scripts: 107 | preinstall: "pkg-scripts/preinstall.sh" 108 | postinstall: "pkg-scripts/postinstall.sh" 109 | preremove: "pkg-scripts/preremove.sh" 110 | 111 | overrides: 112 | rpm: 113 | recommends: 114 | - ca-certificates 115 | - procps-ng 116 | dependencies: 117 | - libcap 118 | scripts: 119 | postinstall: "pkg-scripts/postinstall-rpm.sh" 120 | preremove: "pkg-scripts/preremove-rpm.sh" 121 | deb: 122 | dependencies: 123 | - libcap2-bin 124 | release: 125 | github: 126 | owner: cloudradar-monitoring 127 | name: frontman 128 | draft: true 129 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/cloudradar-monitoring/toml" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestNewMinimumConfig(t *testing.T) { 14 | envURL := "http://foo.bar" 15 | envUser := "foo" 16 | envPass := "bar" 17 | 18 | // TODO: Not sure if this is really a good idea... could mess with other things 19 | os.Setenv("FRONTMAN_HUB_URL", envURL) 20 | os.Setenv("FRONTMAN_HUB_USER", envUser) 21 | os.Setenv("FRONTMAN_HUB_PASSWORD", envPass) 22 | 23 | mvc := NewMinimumConfig() 24 | 25 | assert.Equal(t, envURL, mvc.HubURL, "HubURL should be set from env") 26 | assert.Equal(t, envUser, mvc.HubUser, "HubUser should be set from env") 27 | assert.Equal(t, envPass, mvc.HubPassword, "HubPassword should be set from env") 28 | 29 | // Unset in the end for cleanup 30 | os.Unsetenv("FRONTMAN_HUB_URL") 31 | os.Unsetenv("FRONTMAN_HUB_USER") 32 | os.Unsetenv("FRONTMAN_HUB_PASSWORD") 33 | } 34 | 35 | func TestTryUpdateConfigFromFile(t *testing.T) { 36 | cfg := NewConfig() 37 | 38 | const sampleConfig = ` 39 | pid = "/pid" 40 | sleep = 1.23 41 | ignore_ssl_errors = true 42 | ` 43 | 44 | tmpFile, err := ioutil.TempFile("", "") 45 | assert.Nil(t, err) 46 | defer os.Remove(tmpFile.Name()) 47 | 48 | err = ioutil.WriteFile(tmpFile.Name(), []byte(sampleConfig), 0600) 49 | assert.Nil(t, err) 50 | 51 | err = cfg.TryUpdateConfigFromFile(tmpFile.Name()) 52 | assert.Nil(t, err) 53 | 54 | assert.Equal(t, "/pid", cfg.PidFile) 55 | assert.Equal(t, 1.23, cfg.Sleep) 56 | assert.Equal(t, true, cfg.IgnoreSSLErrors) 57 | 58 | // make sure default values are propagated 59 | assert.Equal(t, []string{"uname", "os_kernel", "os_family", "os_arch", "cpu_model", "fqdn", "memory_total_B"}, cfg.HostInfo) 60 | } 61 | 62 | func TestHandleAllConfigSetup(t *testing.T) { 63 | t.Run("config-file-does-exist", func(t *testing.T) { 64 | const sampleConfig = ` 65 | pid = "/pid" 66 | sleep = 1.0 67 | ignore_ssl_errors = true 68 | ` 69 | 70 | tmpFile, err := ioutil.TempFile("", "") 71 | assert.Nil(t, err) 72 | defer os.Remove(tmpFile.Name()) 73 | 74 | err = ioutil.WriteFile(tmpFile.Name(), []byte(sampleConfig), 0755) 75 | assert.Nil(t, err) 76 | 77 | config, err := HandleAllConfigSetup(tmpFile.Name()) 78 | assert.Nil(t, err) 79 | 80 | assert.Equal(t, "/pid", config.PidFile) 81 | assert.Equal(t, 1.0, config.Sleep) 82 | assert.Equal(t, true, config.IgnoreSSLErrors) 83 | }) 84 | 85 | t.Run("config-file-does-not-exist", func(t *testing.T) { 86 | // Create a temp file to get a file path we can use for temp 87 | // config generation. But delete it so we can actually write our 88 | // config file under the path. 89 | tmpFile, err := ioutil.TempFile("", "") 90 | assert.Nil(t, err) 91 | configFilePath := tmpFile.Name() 92 | err = tmpFile.Close() 93 | assert.Nil(t, err) 94 | 95 | err = os.Remove(tmpFile.Name()) 96 | assert.Nil(t, err) 97 | 98 | _, err = HandleAllConfigSetup(configFilePath) 99 | assert.Nil(t, err) 100 | 101 | _, err = os.Stat(configFilePath) 102 | assert.Nil(t, err) 103 | 104 | mvc := NewMinimumConfig() 105 | loadedMVC := &MinValuableConfig{} 106 | _, err = toml.DecodeFile(configFilePath, loadedMVC) 107 | assert.Nil(t, err) 108 | 109 | if !assert.ObjectsAreEqual(*mvc, *loadedMVC) { 110 | t.Errorf("expected %+v, got %+v", *mvc, *loadedMVC) 111 | } 112 | }) 113 | } 114 | 115 | func TestSecToDuration(t *testing.T) { 116 | assert.Equal(t, 150*time.Millisecond, secToDuration(0.15)) 117 | } 118 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/gorilla/handlers" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | func pingHandler(w http.ResponseWriter, req *http.Request) { 16 | w.Header().Set("Content-Type", "application/json") 17 | _, _ = w.Write([]byte(`{"alive": true}`)) 18 | } 19 | 20 | func (fm *Frontman) checkHandler(w http.ResponseWriter, req *http.Request) { 21 | if req.Method != "POST" { 22 | w.WriteHeader(http.StatusMethodNotAllowed) 23 | return 24 | } 25 | if req.Header.Get("Content-Type") != "application/json" { 26 | w.WriteHeader(http.StatusInternalServerError) 27 | return 28 | } 29 | 30 | w.Header().Set("Content-Type", "application/json") 31 | 32 | decoder := json.NewDecoder(req.Body) 33 | var inputConfig Input 34 | err := decoder.Decode(&inputConfig) 35 | 36 | if err != nil { 37 | logrus.Errorf("json decode error: '%s'", err.Error()) 38 | w.WriteHeader(http.StatusInternalServerError) 39 | return 40 | } 41 | 42 | fm.resultsLock.Lock() 43 | 44 | // perform the checks, collect result and pass it back as json 45 | fm.resultsChan = make(chan Result, 100) 46 | fm.processInput(inputConfig.asChecks(), false) 47 | 48 | // close it so we can iterate it 49 | close(fm.resultsChan) 50 | 51 | res := []Result{} 52 | for elem := range fm.resultsChan { 53 | res = append(res, elem) 54 | } 55 | 56 | fm.resultsLock.Unlock() 57 | 58 | enc, _ := json.Marshal(res) 59 | _, _ = w.Write(enc) 60 | } 61 | 62 | func (listener *HTTPListenerConfig) middlewareLogging(h http.Handler) http.Handler { 63 | var logFile *os.File 64 | if listener.HTTPAccessLog != "" { 65 | absFile, err := filepath.Abs(listener.HTTPAccessLog) 66 | if err != nil { 67 | logrus.Fatal(err) 68 | } 69 | path := filepath.Dir(absFile) 70 | if _, err := os.Stat(path); os.IsNotExist(err) { 71 | logrus.Info("Creating directory for http log:", path) 72 | err = os.MkdirAll(path, os.ModePerm) 73 | if err != nil { 74 | logrus.Fatal(err) 75 | } 76 | } 77 | logFile, err = os.OpenFile(absFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) 78 | if err != nil { 79 | logrus.Error(err) 80 | } 81 | } else { 82 | logFile = os.Stdout 83 | } 84 | return handlers.LoggingHandler(logFile, h) 85 | } 86 | 87 | func (listener HTTPListenerConfig) middlewareAuth(fn http.HandlerFunc) http.HandlerFunc { 88 | return func(w http.ResponseWriter, r *http.Request) { 89 | user, pass, _ := r.BasicAuth() 90 | if user != listener.HTTPAuthUser || pass != listener.HTTPAuthPassword { 91 | http.Error(w, "Unauthorized.", http.StatusUnauthorized) 92 | return 93 | } 94 | fn(w, r) 95 | } 96 | } 97 | 98 | // ServeWeb starts the webserver as configured under [http_listener] section of frontman.conf 99 | func (fm *Frontman) ServeWeb() error { 100 | pos := strings.Index(fm.Config.HTTPListener.HTTPListen, "://") 101 | if pos == -1 { 102 | return fmt.Errorf("invalid address in http_listen: '%s'", fm.Config.HTTPListener.HTTPListen) 103 | } 104 | protocol := fm.Config.HTTPListener.HTTPListen[0:pos] 105 | address := fm.Config.HTTPListener.HTTPListen[pos+3:] 106 | logrus.Info("http_listener listening on ", protocol+"://"+address) 107 | http.Handle("/ping", fm.Config.HTTPListener.middlewareLogging(http.HandlerFunc(pingHandler))) 108 | http.Handle("/check", fm.Config.HTTPListener.middlewareLogging(fm.Config.HTTPListener.middlewareAuth(fm.checkHandler))) 109 | var err error 110 | switch protocol { 111 | case "http": 112 | err = http.ListenAndServe(address, nil) 113 | case "https": 114 | err = http.ListenAndServeTLS(address, fm.Config.HTTPListener.HTTPTLSCert, fm.Config.HTTPListener.HTTPTLSKey, nil) 115 | default: 116 | return fmt.Errorf("invalid protocol: '%s'", protocol) 117 | } 118 | return err 119 | } 120 | -------------------------------------------------------------------------------- /pkg-scripts/msi-templates/WixUI_HK.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 1 29 | "1"]]> 30 | 31 | 1 32 | 33 | NOT Installed 40 | Installed AND PATCH 41 | 42 | 1 43 | LicenseAccepted = "1" 44 | 45 | 1 46 | 1 47 | NOT WIXUI_DONTVALIDATEPATH 48 | "1"]]> 49 | 50 | 51 | 1 52 | 53 | 1 54 | 1 55 | 56 | 1 57 | 58 | 59 | 1 60 | 61 | 1 62 | 1 63 | 1 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /check.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "encoding/json" 5 | "sync" 6 | 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type Check interface { 11 | // run always returns a *Result, even in case of failure 12 | run(fm *Frontman) (*Result, error) 13 | 14 | // uniqueUD returns the check UUID 15 | uniqueID() string 16 | } 17 | 18 | type Input struct { 19 | ServiceChecks []ServiceCheck `json:"serviceChecks"` 20 | WebChecks []WebCheck `json:"webChecks"` 21 | SNMPChecks []SNMPCheck `json:"snmpChecks,omitempty"` 22 | } 23 | 24 | type ServiceCheck struct { 25 | UUID string `json:"checkUuid"` 26 | Check ServiceCheckData `json:"check"` 27 | } 28 | 29 | type ServiceCheckData struct { 30 | Connect string `json:"connect,omitempty"` 31 | Service string `json:"service,omitempty"` 32 | Protocol string `json:"protocol,omitempty"` 33 | Port json.Number `json:"port,omitempty"` 34 | } 35 | 36 | type WebCheck struct { 37 | UUID string `json:"checkUuid"` 38 | Check WebCheckData `json:"check"` 39 | } 40 | 41 | type WebCheckData struct { 42 | Method string `json:"method"` 43 | URL string `json:"url"` 44 | PostData string `json:"postData,omitempty"` 45 | ExpectedHTTPStatus int `json:"expectedHttpStatus,omitempty"` 46 | SearchHTMLSource bool `json:"searchHtmlSource"` 47 | ExpectedPattern string `json:"expectedPattern,omitempty"` 48 | ExpectedPatternPresence string `json:"expectedPatternPresence,omitempty"` 49 | DontFollowRedirects bool `json:"dontFollowRedirects"` 50 | IgnoreSSLErrors *bool `json:"ignoreSSLErrors,omitempty"` 51 | Timeout float64 `json:"timeout,omitempty"` 52 | Headers map[string]string `json:"headers,omitempty"` 53 | } 54 | 55 | type SNMPCheck struct { 56 | UUID string `json:"checkUuid"` 57 | Check SNMPCheckData `json:"check"` 58 | } 59 | 60 | type SNMPCheckData struct { 61 | Connect string `json:"connect"` 62 | Port uint16 `json:"port"` 63 | Timeout float64 `json:"timeout"` 64 | Protocol string `json:"protocol"` 65 | Community string `json:"community,omitempty"` // v1, v2 66 | Preset string `json:"preset"` 67 | Oids []string `json:"oids,omitempty"` 68 | SecurityLevel string `json:"security_level,omitempty"` // v3 69 | Username string `json:"username,omitempty"` // v3 70 | AuthenticationProtocol string `json:"authentication_protocol,omitempty"` // v3 71 | AuthenticationPassword string `json:"authentication_password,omitempty"` // v3 72 | PrivacyProtocol string `json:"privacy_protocol,omitempty"` // v3 73 | PrivacyPassword string `json:"privacy_password,omitempty"` // v3 74 | 75 | // values used by "oid" preset 76 | Oid string `json:"oid,omitempty"` 77 | Name string `json:"name,omitempty"` 78 | ValueType string `json:"value_type,omitempty"` /// auto (default), hex, delta, delta_per_sec 79 | Unit string `json:"unit,omitempty"` 80 | } 81 | 82 | // used to keep track of in-progress checks being run 83 | type inProgressChecks struct { 84 | mutex sync.RWMutex 85 | uuids map[string]bool 86 | } 87 | 88 | func newIPC() inProgressChecks { 89 | return inProgressChecks{ 90 | uuids: make(map[string]bool), 91 | } 92 | } 93 | 94 | func (ipc *inProgressChecks) add(uuid string) { 95 | ipc.mutex.Lock() 96 | defer ipc.mutex.Unlock() 97 | ipc.uuids[uuid] = true 98 | } 99 | 100 | func (ipc *inProgressChecks) remove(uuid string) { 101 | ipc.mutex.Lock() 102 | defer ipc.mutex.Unlock() 103 | delete(ipc.uuids, uuid) 104 | } 105 | 106 | func (ipc *inProgressChecks) isInProgress(uuid string) bool { 107 | ipc.mutex.RLock() 108 | defer ipc.mutex.RUnlock() 109 | 110 | if b, ok := ipc.uuids[uuid]; ok && b { 111 | return true 112 | } 113 | return false 114 | } 115 | 116 | // returns the slice index for the first check in `checks` that is not already in progress, false if none found 117 | func (fm *Frontman) getIndexOfFirstCheckNotInProgress() (int, bool) { 118 | fm.checksLock.RLock() 119 | defer fm.checksLock.RUnlock() 120 | 121 | for idx, c := range fm.checks { 122 | if !fm.ipc.isInProgress(c.uniqueID()) { 123 | return idx, true 124 | } 125 | logrus.Infof("Skipping request for check %v. Check still in progress.", c.uniqueID()) 126 | } 127 | return 0, false 128 | } 129 | -------------------------------------------------------------------------------- /synology-spk/2_create_project/scripts/installer: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Package 4 | PACKAGE="frontman" 5 | DNAME="Frontman" 6 | 7 | TEMP_STORAGE_DIR="${SYNOPKG_TEMP_UPGRADE_FOLDER}" 8 | INSTALL_DIR="/usr/local/${PACKAGE}" 9 | APP_DIR="${INSTALL_DIR}/frontman" 10 | SSS="/var/packages/${DNAME}/scripts/start-stop-status" 11 | PATH="${INSTALL_DIR}:${PATH}" 12 | 13 | SERVICETOOL="/usr/syno/bin/servicetool" 14 | FWPORTS="/var/packages/${DNAME}/scripts/${PACKAGE}.sc" 15 | 16 | FILE_CREATE_LOG="${APP_DIR}/wizard_create_log" 17 | LOG_FILE="/var/log/frontman.log" 18 | PACKAGE_LOG="/var/log/synopkg.log" 19 | 20 | preinst () 21 | { 22 | exit 0 23 | } 24 | 25 | postinst () 26 | { 27 | # Link 28 | ln -s ${SYNOPKG_PKGDEST} ${INSTALL_DIR} 29 | 30 | # to create log at each start 31 | if [ "${wizard_create_log}" == "true" ]; then 32 | touch ${FILE_CREATE_LOG} 33 | fi 34 | 35 | # Add firewall config 36 | ${SERVICETOOL} --install-configure-file --package ${FWPORTS} >> /dev/null 37 | 38 | # install default config 39 | mkdir -p /etc/frontman 40 | cp -f ${APP_DIR}/frontman.default.conf /etc/frontman/frontman.conf 41 | 42 | # apply config values from WIZARD_UIFILES/install_uifile 43 | sed -i "s#CONFIG_HUB_URL#${CONFIG_HUB_URL}#g" /etc/frontman/frontman.conf 44 | sed -i "s#CONFIG_HUB_USER#${CONFIG_HUB_USER}#g" /etc/frontman/frontman.conf 45 | sed -i "s#CONFIG_HUB_PASSWORD#${CONFIG_HUB_PASSWORD}#g" /etc/frontman/frontman.conf 46 | 47 | mkdir -p /var/log/frontman 48 | chown frontman:frontman /var/log/frontman 49 | 50 | # allow ICMP pings 51 | cat << EOF > /usr/local/etc/rc.d/frontman-ping.sh 52 | #!/bin/sh 53 | if [ \$1 = "start" ] 54 | then sysctl -w net.ipv4.ping_group_range="0 2147483647" 55 | fi 56 | EOF 57 | chmod 755 /usr/local/etc/rc.d/frontman-ping.sh 58 | sysctl -w net.ipv4.ping_group_range="0 2147483647" > /dev/null 59 | 60 | exit 0 61 | } 62 | 63 | preuninst () 64 | { 65 | # Stop the package 66 | ${SSS} stop > /dev/null 67 | 68 | # Remove firewall config 69 | if [ "${SYNOPKG_PKG_STATUS}" == "UNINSTALL" ]; then 70 | ${SERVICETOOL} --remove-configure-file --package ${PACKAGE}.sc >> /dev/null 71 | fi 72 | 73 | exit 0 74 | } 75 | 76 | postuninst () 77 | { 78 | # Remove link 79 | rm -f ${INSTALL_DIR} 80 | 81 | # remove log file 82 | rm -f ${LOG_FILE} 83 | 84 | # remove ICMP ping settings 85 | rm -f /usr/local/etc/rc.d/frontman-ping.sh 86 | 87 | exit 0 88 | } 89 | 90 | preupgrade () 91 | { 92 | # Stop the package 93 | ${SSS} stop > /dev/null 94 | 95 | ret=0 96 | # backup the data 97 | log "Backup data" ${SYNOPKG_OLD_PKGVER} 98 | for dir in ${APP_DIR}/*/ ; do 99 | logBegin "rsync ${dir%*/} to ${TEMP_STORAGE_DIR}/" ${SYNOPKG_OLD_PKGVER} 100 | rsync -a ${dir%*/} ${TEMP_STORAGE_DIR}/ 101 | error_code=$? 102 | logEnd "rsync ${dir%*/} to ${TEMP_STORAGE_DIR}/" $error_code ${SYNOPKG_OLD_PKGVER} 103 | if [ ! "$error_code" -eq "0" ]; then 104 | $ret=1 105 | echo "Could not backup data $dir. Please ensure there is sufficient space." >> $SYNOPKG_TEMP_LOGFILE 106 | fi 107 | done 108 | if [ -f ${FILE_CREATE_LOG} ]; then 109 | cp -a ${FILE_CREATE_LOG} ${TEMP_STORAGE_DIR} 110 | fi 111 | 112 | exit $ret 113 | } 114 | 115 | postupgrade () 116 | { 117 | ret=0 118 | # restore the data 119 | log "Restore data" ${SYNOPKG_PKGVER} 120 | for dir in ${TEMP_STORAGE_DIR}/*/ ; do 121 | logBegin "rsync ${dir%*/} to ${APP_DIR}/" ${SYNOPKG_PKGVER} 122 | rsync -a ${dir%*/} ${APP_DIR}/ 123 | error_code=$? 124 | logEnd "rsync ${dir%*/} to ${APP_DIR}/" $error_code ${SYNOPKG_PKGVER} 125 | if [ ! "$error_code" -eq "0" ]; then 126 | $ret=1 127 | echo "Could not restore data from $dir. " >> $SYNOPKG_TEMP_LOGFILE 128 | fi 129 | done 130 | if [ -f ${TEMP_STORAGE_DIR}/wizard_create_log ]; then 131 | logBegin "copy ${TEMP_STORAGE_DIR}/wizard_create_log to ${APP_DIR}/" ${SYNOPKG_PKGVER} 132 | cp -a ${TEMP_STORAGE_DIR}/wizard_create_log ${APP_DIR}/ 133 | logEnd "copy ${TEMP_STORAGE_DIR}/wizard_create_log to ${APP_DIR}/" $? ${SYNOPKG_PKGVER} 134 | fi 135 | 136 | if [ ! "$ret" -eq "0" ]; then 137 | echo "Data restore failed. Please uninstall, perform new installation and restore data manually from your backup." >> $SYNOPKG_TEMP_LOGFILE 138 | fi 139 | 140 | exit $ret 141 | } 142 | 143 | log () 144 | { 145 | msg=$1 146 | version=$2 147 | echo "$(date +"%Y/%m/%d %T") upgrade Frontman $version $msg" >> ${PACKAGE_LOG} 148 | } 149 | 150 | logBegin () 151 | { 152 | msg=$1 153 | version=$2 154 | echo "$(date +"%Y/%m/%d %T") upgrade Frontman $version Begin $msg" >> ${PACKAGE_LOG} 155 | } 156 | 157 | logEnd () 158 | { 159 | msg=$1 160 | code=$2 161 | version=$3 162 | echo "$(date +"%Y/%m/%d %T") upgrade Frontman $version End $msg ret=[$code]" >> ${PACKAGE_LOG} 163 | } 164 | -------------------------------------------------------------------------------- /frontman.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "crypto/x509" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "runtime" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/cloudradar-monitoring/selfupdate" 15 | "github.com/kardianos/service" 16 | "github.com/pkg/errors" 17 | "github.com/sirupsen/logrus" 18 | 19 | "github.com/cloudradar-monitoring/frontman/pkg/stats" 20 | ) 21 | 22 | // variables set on build. Example: 23 | // go build -o frontman -ldflags="-X main.Version=$(git --git-dir=src/github.com/cloudradar-monitoring/frontman/.git describe --always --long --dirty --tag)" github.com/cloudradar-monitoring/frontman/cmd/frontman 24 | var ( 25 | Version string 26 | SelfUpdatesFeedURL = "https://repo.cloudradar.io/windows/frontman/feed/rolling" 27 | ) 28 | 29 | type Frontman struct { 30 | Config *Config 31 | ConfigLocation string 32 | 33 | stats *stats.FrontmanStats 34 | statsLock sync.Mutex 35 | 36 | HealthCheckPassedPreviously bool 37 | 38 | selfUpdater *selfupdate.Updater 39 | 40 | hubClient *http.Client 41 | hostInfoSent bool 42 | 43 | // local cached results in case the hub is temporarily offline 44 | offlineResultsBuffer []Result 45 | offlineResultsLock sync.Mutex 46 | 47 | rootCAs *x509.CertPool 48 | version string 49 | 50 | failedNodeLock sync.Mutex 51 | failedNodes map[string]time.Time 52 | failedNodeCache map[string][]byte 53 | 54 | forwardLog *os.File 55 | 56 | serviceConfig service.Config 57 | 58 | // current checks queue 59 | checks []Check 60 | checksLock sync.RWMutex 61 | 62 | // in-progress checks 63 | ipc inProgressChecks 64 | 65 | // completed check results to be sent to hub 66 | results []Result 67 | 68 | // in case HUB server will hang on response we will need a buffer to continue perform checks 69 | resultsChan chan Result 70 | 71 | resultsLock sync.RWMutex 72 | 73 | previousSNMPBandwidthMeasure []snmpBandwidthMeasure 74 | previousSNMPOidDeltaMeasure []snmpOidDeltaMeasure 75 | previousSNMPPorterrorsMeasure []snmpPorterrorsMeasure 76 | 77 | TerminateQueue sync.WaitGroup 78 | 79 | // current number of send result threads 80 | senderThreads int64 81 | 82 | // interrupt handler, close it to shut down frontman 83 | InterruptChan chan struct{} 84 | 85 | // send true to signal completion 86 | DoneChan chan bool 87 | } 88 | 89 | func New(cfg *Config, cfgPath, version string) (*Frontman, error) { 90 | if version == "" { 91 | version = "{undefined}" 92 | } 93 | fm := &Frontman{ 94 | Config: cfg, 95 | ConfigLocation: cfgPath, 96 | stats: &stats.FrontmanStats{}, 97 | HealthCheckPassedPreviously: true, 98 | hostInfoSent: false, 99 | version: version, 100 | failedNodes: make(map[string]time.Time), 101 | failedNodeCache: make(map[string][]byte), 102 | TerminateQueue: sync.WaitGroup{}, 103 | ipc: newIPC(), 104 | resultsChan: make(chan Result, 100), 105 | InterruptChan: make(chan struct{}), 106 | DoneChan: make(chan bool), 107 | } 108 | 109 | if rootCertsPath != "" { 110 | if _, err := os.Stat(rootCertsPath); err == nil { 111 | certPool := x509.NewCertPool() 112 | 113 | b, err := ioutil.ReadFile(rootCertsPath) 114 | if err != nil { 115 | logrus.Error("Failed to read cacert.pem: ", err.Error()) 116 | } else { 117 | ok := certPool.AppendCertsFromPEM(b) 118 | if ok { 119 | fm.rootCAs = certPool 120 | } 121 | } 122 | } 123 | } 124 | 125 | if err := fm.Config.sanitize(); err != nil { 126 | logrus.Error(err) 127 | } 128 | 129 | fm.configureLogger() 130 | 131 | fm.initHubClient() 132 | 133 | err := fm.configureAutomaticSelfUpdates() 134 | if err != nil { 135 | logrus.Error(err.Error()) 136 | return nil, err 137 | } 138 | 139 | return fm, nil 140 | } 141 | 142 | func (fm *Frontman) configureAutomaticSelfUpdates() error { 143 | if !fm.Config.Updates.Enabled { 144 | return nil 145 | } 146 | if Version == "" { 147 | logrus.Warn("skipping configureAutomaticSelfUpdates, Version is not set") 148 | return nil 149 | } 150 | 151 | updatesConfig := selfupdate.DefaultConfig() 152 | updatesConfig.AppName = "frontman" 153 | updatesConfig.SigningCertificatedName = "cloudradar GmbH" 154 | updatesConfig.CurrentVersion = Version 155 | updatesConfig.CheckInterval = fm.Config.Updates.GetCheckInterval() 156 | updatesConfig.UpdatesFeedURL = fm.Config.Updates.URL 157 | logrus.Debugf("using %s as self-updates feed URL", updatesConfig.UpdatesFeedURL) 158 | 159 | err := selfupdate.Configure(updatesConfig) 160 | if err != nil { 161 | return errors.Wrapf(err, "invalid configuration for self-update") 162 | } 163 | 164 | selfupdate.SetLogger(logrus.StandardLogger()) 165 | 166 | return nil 167 | } 168 | 169 | // returns an user agent string 170 | func (fm *Frontman) userAgent() string { 171 | parts := strings.Split(fm.version, "-") 172 | return fmt.Sprintf("Frontman v%s %s %s", parts[0], runtime.GOOS, runtime.GOARCH) 173 | } 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Frontman - at a glance 2 | Frontman is a general-purpose monitoring proxy that performs checks on foreign hosts. 3 | The main goal is to check services and performs other checks, where no logon-rights are needed for. 4 | It's expected to be used in any kind of monitoring solutions, where agent-less checks must be executed over the network. 5 | Frontman is a robust check executor that can work in the background of any monitoring solution. It enables developers to build custom solutions. Frontman can be controlled by local Json files or via Json-based HTTP APIs. 6 | 7 | ## Credits 8 | 9 | 10 | 11 | 12 | 13 | 14 | Project co-financed by the European Regional Development Fundunder the Innovative Economy Operational Programme. Innovation grants.We invest in your future. 15 | 16 | 17 | 18 | 19 | 20 | ## What kind of checks frontman can perform 21 | * [ICMP ping](https://github.com/cloudradar-monitoring/frontman/blob/master/example.json#L53) 22 | * [TCP/UDP – check connection on port](https://github.com/cloudradar-monitoring/frontman/blob/master/example.json#L68) 23 | * [TCP/UDP – service check (check the connection and the common output pattern)](https://github.com/cloudradar-monitoring/frontman/blob/master/example.json#L77) 24 | * HTTP(S) 25 | * FTP(S) 26 | * FTP(S) 27 | * SMTP(S) 28 | * POP3(S) 29 | * SSH 30 | * NNTP 31 | * LDAP 32 | * [SIP](https://github.com/cloudradar-monitoring/frontman/blob/master/example.json#L140) 33 | * [IAX2](https://github.com/cloudradar-monitoring/frontman/blob/master/example.json#L143) 34 | * [SSL – check the certificate validity and expiration date](https://github.com/cloudradar-monitoring/frontman/blob/master/example.json#L119) 35 | * HTTP web checks 36 | * [Check status](https://github.com/cloudradar-monitoring/frontman/blob/master/example.json#L4) 37 | * [Match raw HTML pattern](https://github.com/cloudradar-monitoring/frontman/blob/master/example.json#L31) 38 | * [Match extracted text patter](https://github.com/cloudradar-monitoring/frontman/blob/master/example.json#L28) 39 | 40 | 41 | ## Run the example 42 | 43 | ```bash 44 | ./frontman -i src/github.com/cloudradar-monitoring/frontman/example.json -o result.out 45 | ``` 46 | Use `ctrl-c` to stop it 47 | 48 | ## Command line Usage 49 | ``` 50 | Usage of frontman: 51 | -c config file path (default depends on OS) 52 | -d daemonize – run the process in background 53 | -i JSON file to read the list (required if no hub_url specified in the config) 54 | -o file to write the results (default ./results.out) 55 | -r one run only – perform checks once and exit. Overwrites output file 56 | -s username to install and start the system service 57 | -u stop and uninstall the system service 58 | -p print the active config 59 | -v log level – overrides the level in config file (values "error","info","debug") (default "error") 60 | -version show the frontman version 61 | ``` 62 | ## Configuration 63 | On first run frontman will automatically create the config file and fill it with the default value. 64 | 65 | Default config locations: 66 | * Mac OS: `~/.frontman/frontman.conf` 67 | * Windows: `./frontman.conf` 68 | * UNIX: `/etc/frontman/frontman.conf` 69 | 70 | If some of the fields are missing in the config `frontman` will use the defaults. 71 | To print the active config you can use `frontman -p` 72 | 73 | Also you may want to check the [example config](https://github.com/cloudradar-monitoring/frontman/blob/master/example.config.toml) that contains comments on each field. 74 | 75 | ## Logs location 76 | * Mac OS: `~/.frontman/frontman.log` 77 | * Windows: `./frontman.log` 78 | * UNIX: `/etc/frontman/frontman.conf` 79 | 80 | ## How to build from sources 81 | - [Install Golang 1.9 or newer](https://golang.org/dl/) 82 | ```bash 83 | go get -d -u github.com/cloudradar-monitoring/frontman 84 | go build -o frontman -ldflags="-X main.version=$(git --git-dir=src/github.com/cloudradar-monitoring/frontman/.git describe --always --long --dirty --tag)" github.com/cloudradar-monitoring/frontman/cmd/frontman 85 | ``` 86 | 87 | ## Build binaries and deb/rpm packages 88 | – Install [goreleaser](https://goreleaser.com/introduction/) 89 | ```bash 90 | make goreleaser-snapshot 91 | ``` 92 | 93 | ## Build MSI package 94 | Should be done on Windows machine 95 | - [Download go-msi](https://github.com/cloudradar-monitoring/go-msi/releases) and put it in the `C:\Program Files\go-msi` 96 | - Open command prompt(cmd.exe or powershell) 97 | - Go to frontman directory `cd path_to_directory` 98 | - Run `make goreleaser-snapshot` to build binaries 99 | - Run `build-win.bat` 100 | 101 | ## Versioning model 102 | Frontman uses `..` model for compatibility with a maximum number of package managers. 103 | 104 | Starting from version 1.2.0 packages with even `` number are considered stable. 105 | 106 | ## Running as a docker container 107 | Check [dockerhub](https://cloud.docker.com/u/cloudradario/repository/docker/cloudradario/frontman) for available images. 108 | 109 | ### Passing credentials 110 | Username and password need to be configured via environment variables. You can pass them using the `-e` flags. 111 | `docker run -d -e FRONTMAN_HUB_USER=YOUR_USERNAME -e FRONTMAN_HUB_PASSWORD=YOUR_PASS cloudradario/frontman:1.0.7` 112 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "time" 11 | 12 | "github.com/cloudradar-monitoring/frontman/pkg/stats" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type LogLevel string 17 | 18 | const ( 19 | LogLevelDebug LogLevel = "debug" 20 | LogLevelInfo LogLevel = "info" 21 | LogLevelError LogLevel = "error" 22 | ) 23 | 24 | func (lvl LogLevel) LogrusLevel() logrus.Level { 25 | switch lvl { 26 | case LogLevelDebug: 27 | return logrus.DebugLevel 28 | case LogLevelError: 29 | return logrus.ErrorLevel 30 | default: 31 | return logrus.InfoLevel 32 | } 33 | } 34 | 35 | type logrusFileHook struct { 36 | file *os.File 37 | flag int 38 | chmod os.FileMode 39 | formatter *logrus.TextFormatter 40 | } 41 | 42 | func addLogFileHook(file string, flag int, chmod os.FileMode) error { 43 | dir := filepath.Dir(file) 44 | err := os.MkdirAll(dir, 0755) 45 | if err != nil { 46 | logrus.WithError(err).Errorf("Failed to create the logs dir: '%s'", dir) 47 | } 48 | 49 | plainFormatter := &logrus.TextFormatter{FullTimestamp: true, DisableColors: true} 50 | logFile, err := os.OpenFile(file, flag, chmod) 51 | if err != nil { 52 | return fmt.Errorf("unable to write log file: %s", err.Error()) 53 | } 54 | 55 | hook := &logrusFileHook{logFile, flag, chmod, plainFormatter} 56 | 57 | logrus.AddHook(hook) 58 | 59 | return nil 60 | } 61 | 62 | func (fm *Frontman) addErrorHook() { 63 | hook := &LogrusErrorHook{ 64 | fm: fm, 65 | } 66 | 67 | logrus.AddHook(hook) 68 | } 69 | 70 | // Fire event 71 | func (hook *logrusFileHook) Fire(entry *logrus.Entry) error { 72 | plainformat, err := hook.formatter.Format(entry) 73 | if err != nil { 74 | return err 75 | } 76 | line := string(plainformat) 77 | _, err = hook.file.WriteString(line) 78 | if err != nil { 79 | fmt.Fprintf(os.Stderr, "unable to write file on filehook(entry.String)%v", err) 80 | return err 81 | } 82 | return nil 83 | } 84 | 85 | func (hook *logrusFileHook) Levels() []logrus.Level { 86 | return []logrus.Level{ 87 | logrus.PanicLevel, 88 | logrus.FatalLevel, 89 | logrus.ErrorLevel, 90 | logrus.WarnLevel, 91 | logrus.InfoLevel, 92 | logrus.DebugLevel, 93 | } 94 | } 95 | 96 | // startWritingStats writes fm.Stats every minute to Config.StatsFile 97 | // This method should only be called once 98 | func (fm *Frontman) startWritingStats() { 99 | 100 | // Only start writing out stats if there is a StatsFile configued 101 | if fm.Config.StatsFile == "" { 102 | return 103 | } 104 | 105 | fm.stats.StartedAt = time.Now() 106 | logrus.Debugf("Start writing stats file: %s", fm.Config.StatsFile) 107 | 108 | var buff bytes.Buffer 109 | var err error 110 | 111 | // Make the output indented 112 | encoder := json.NewEncoder(&buff) 113 | encoder.SetIndent("", " ") 114 | 115 | for { 116 | buff.Reset() 117 | time.Sleep(time.Second * 30) 118 | 119 | stats := fm.statsSnapshot() 120 | stats.Uptime = uint64(time.Since(stats.StartedAt).Seconds()) 121 | 122 | err = encoder.Encode(stats) 123 | if err != nil { 124 | logrus.Errorf("Could not encode stats file: %s", err) 125 | continue 126 | } 127 | 128 | err = ioutil.WriteFile(fm.Config.StatsFile, buff.Bytes(), 0755) 129 | if err != nil { 130 | logrus.Errorf("Could not write stats file: %s", err) 131 | return 132 | } 133 | } 134 | } 135 | 136 | // Get snapshot from current stats 137 | func (fm *Frontman) statsSnapshot() stats.FrontmanStats { 138 | fm.statsLock.Lock() 139 | defer fm.statsLock.Unlock() 140 | return *fm.stats 141 | } 142 | 143 | // SetLogLevel sets Log level and corresponding logrus level 144 | func (fm *Frontman) SetLogLevel(lvl LogLevel) { 145 | fm.Config.LogLevel = lvl 146 | logrus.SetLevel(lvl.LogrusLevel()) 147 | } 148 | 149 | type LogrusErrorHook struct { 150 | fm *Frontman 151 | } 152 | 153 | func (h *LogrusErrorHook) Fire(entry *logrus.Entry) error { 154 | h.fm.statsLock.Lock() 155 | h.fm.stats.InternalErrorsTotal++ 156 | h.fm.stats.InternalLastErrorMessage = entry.Message 157 | h.fm.stats.InternalLastErrorTimestamp = uint64(time.Now().Unix()) 158 | h.fm.statsLock.Unlock() 159 | return nil 160 | } 161 | 162 | func (h *LogrusErrorHook) Levels() []logrus.Level { 163 | return []logrus.Level{ 164 | logrus.ErrorLevel, 165 | } 166 | } 167 | 168 | func (fm *Frontman) configureLogger() { 169 | tfmt := logrus.TextFormatter{FullTimestamp: true, DisableColors: true} 170 | logrus.SetFormatter(&tfmt) 171 | 172 | fm.SetLogLevel(fm.Config.LogLevel) 173 | 174 | if fm.Config.LogFile != "" { 175 | err := addLogFileHook(fm.Config.LogFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) 176 | if err != nil { 177 | logrus.Error("Can't write logs to file: ", err.Error()) 178 | } 179 | } 180 | if fm.Config.LogSyslog != "" { 181 | err := addSyslogHook(fm.Config.LogSyslog) 182 | if err != nil { 183 | logrus.Error("Can't set up syslog: ", err.Error()) 184 | } 185 | } 186 | 187 | if fm.Config.Node.ForwardLog != "" { 188 | var err error 189 | fm.forwardLog, err = os.OpenFile(fm.Config.Node.ForwardLog, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) 190 | if err != nil { 191 | logrus.Error("Can't set up forward_log: ", err.Error()) 192 | } 193 | } 194 | 195 | // Add hook to logrus that updates our LastInternalError statistics 196 | // whenever an error log is done 197 | fm.addErrorHook() 198 | 199 | // sets standard logging to /dev/null 200 | devNull, err := os.OpenFile(os.DevNull, os.O_APPEND|os.O_WRONLY, os.ModeAppend) 201 | if err != nil { 202 | logrus.Error("err", err) 203 | } 204 | logrus.SetOutput(devNull) 205 | } 206 | -------------------------------------------------------------------------------- /mockhub.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | // a mock hub used in tests, similar to https://bitbucket.org/cloudradar/debug_hub/ 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "math/rand" 11 | "net/http" 12 | "strconv" 13 | ) 14 | 15 | type MockHub struct { 16 | address string 17 | 18 | // if set, responds with this status code on all requests. useful for testing 401 and such 19 | ResponseStatusCode int 20 | } 21 | 22 | func NewMockHub(address string) *MockHub { 23 | return &MockHub{ 24 | address: address, 25 | } 26 | } 27 | 28 | func (hub *MockHub) URL() string { 29 | return "http://" + hub.address 30 | } 31 | 32 | // returns some mocked checks 33 | func (hub *MockHub) indexHandler(w http.ResponseWriter, r *http.Request) { 34 | if hub.ResponseStatusCode != 0 { 35 | log.Printf("Responding to request with status code %d", hub.ResponseStatusCode) 36 | w.WriteHeader(hub.ResponseStatusCode) 37 | return 38 | } 39 | switch r.Method { 40 | case "GET": 41 | hub.getHandler(w, r) 42 | case "POST": 43 | hub.postHandler(w, r) 44 | } 45 | } 46 | 47 | func (hub *MockHub) postHandler(w http.ResponseWriter, r *http.Request) { 48 | // simulate reset content 49 | //w.WriteHeader(205) 50 | 51 | // simulate slow hub 52 | //time.Sleep(500 * time.Millisecond) 53 | 54 | data, err := ioutil.ReadAll(r.Body) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | 59 | log.Println("MockHub: got post data", string(data)) 60 | } 61 | 62 | func (hub *MockHub) getHandler(w http.ResponseWriter, r *http.Request) { 63 | w.Header().Set("Content-Type", "application/json") 64 | 65 | query := r.URL.Query() 66 | serviceChecksS := query.Get("serviceChecks") 67 | webChecksS := query.Get("webChecks") 68 | 69 | serviceChecks, err := strconv.ParseInt(serviceChecksS, 10, 64) 70 | if err != nil { 71 | serviceChecks = 5 72 | } 73 | 74 | webChecks, err := strconv.ParseInt(webChecksS, 10, 64) 75 | if err != nil { 76 | webChecks = 5 77 | } 78 | 79 | log.Println("responding with", serviceChecks, "serviceChecks and", webChecks, "webChecks") 80 | 81 | checks := Input{ 82 | ServiceChecks: mockServiceChecks(int(serviceChecks)), 83 | WebChecks: mockWebChecks(int(webChecks)), 84 | } 85 | 86 | data, _ := json.Marshal(checks) 87 | w.Write(data) 88 | } 89 | 90 | func (hub *MockHub) Serve() { 91 | 92 | http.HandleFunc("/", hub.indexHandler) 93 | log.Println("mock hub listening at", hub.URL()) 94 | http.ListenAndServe(hub.address, nil) 95 | } 96 | 97 | func mockServiceChecks(n int) []ServiceCheck { 98 | res := []ServiceCheck{} 99 | for i := 0; i < n; i++ { 100 | res = append(res, randomServiceCheck()) 101 | } 102 | return res 103 | } 104 | 105 | func mockWebChecks(n int) []WebCheck { 106 | res := []WebCheck{} 107 | for i := 0; i < n; i++ { 108 | if i%5 == 0 { 109 | // every fifth web check should be a lame web check 110 | res = append(res, lameWebCheck()) 111 | } else { 112 | res = append(res, randomWebCheck()) 113 | } 114 | } 115 | return res 116 | } 117 | 118 | func randomWebCheck() WebCheck { 119 | methods := []string{"get", "post", "head"} 120 | statuses := []int{200, 404} 121 | patterns := []string{"running", "welcome", "yyy"} 122 | return WebCheck{ 123 | UUID: randomUUID(), 124 | Check: WebCheckData{ 125 | URL: fmt.Sprintf("https://h%d.hostgum.eu/", rand.Intn(1000)), 126 | Method: methods[rand.Intn(len(methods))], 127 | ExpectedHTTPStatus: statuses[rand.Intn(len(statuses))], 128 | ExpectedPattern: patterns[rand.Intn(len(patterns))], 129 | }, 130 | } 131 | } 132 | 133 | func lameWebCheck() WebCheck { 134 | return WebCheck{ 135 | UUID: randomUUID(), 136 | Check: WebCheckData{ 137 | URL: fmt.Sprintf("https://h1.hostgum.eu/sleep.php?t=%d", 5+rand.Intn(45)), 138 | Method: "get", 139 | ExpectedHTTPStatus: 200, 140 | ExpectedPattern: "slept", 141 | Timeout: randomFloat(30, 60), 142 | }, 143 | } 144 | } 145 | 146 | func randomFloat(min, max float64) float64 { 147 | return min + rand.Float64()*(max-min) 148 | } 149 | 150 | func randomUUID() string { 151 | b := make([]byte, 16) 152 | _, err := rand.Read(b) 153 | if err != nil { 154 | log.Fatal(err) 155 | } 156 | return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) 157 | } 158 | 159 | func randomServiceCheck() ServiceCheck { 160 | switch rand.Intn(2) { 161 | case 0: 162 | return randomPingCheck() 163 | default: 164 | return randomTcpCheck() 165 | } 166 | } 167 | 168 | func randomPingCheck() ServiceCheck { 169 | return ServiceCheck{ 170 | UUID: randomUUID(), 171 | Check: ServiceCheckData{ 172 | Connect: randomConnect(), 173 | Service: "ping", 174 | Protocol: "icmp", 175 | }, 176 | } 177 | } 178 | 179 | func randomTcpCheck() ServiceCheck { 180 | services := []string{ 181 | "http", 182 | "https", 183 | "pop3", 184 | "imap", 185 | "smtp", 186 | } 187 | return ServiceCheck{ 188 | UUID: randomUUID(), 189 | Check: ServiceCheckData{ 190 | Connect: fmt.Sprintf("h%d.hostgum.eu", rand.Intn(1000)), 191 | Protocol: "tcp", 192 | Service: services[rand.Intn(len(services))], 193 | }, 194 | } 195 | } 196 | 197 | func randomConnect() string { 198 | pool := []string{ 199 | "www.google.com", 200 | "8.8.8.8", 201 | "8.8.4.4", 202 | "1.1.1.1", 203 | "h1.hostgum.eu", 204 | "lameduck.hostgum.eu", 205 | "not_exists_domain1234.com", 206 | "github.com", 207 | "dns.google", 208 | "1.11.192.227", 209 | "202.181.242.131", 210 | "45.225.123.88", 211 | "212.91.32.6", 212 | "ns1.artechinfo.in", 213 | "pb6abf7bd.szokff01.ap.so-net.ne.jp", 214 | "211.105.7.5", 215 | "dns.prhs.ptc.edu.tw", 216 | "163.24.162.3", 217 | } 218 | return pool[rand.Intn(len(pool))] 219 | } 220 | -------------------------------------------------------------------------------- /udp.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net" 8 | "strconv" 9 | "strings" 10 | "text/template" 11 | "time" 12 | 13 | "github.com/cloudradar-monitoring/frontman/pkg/iax" 14 | "github.com/cloudradar-monitoring/frontman/pkg/utils" 15 | ) 16 | 17 | func (fm *Frontman) runUDPCheck(hostname string, port int, service string) (MeasurementsMap, error) { 18 | // Check if we have to autodetect port by service name 19 | if port <= 0 { 20 | // Lookup service by default port 21 | p, exists := defaultPortByService[service] 22 | if !exists { 23 | return nil, fmt.Errorf("failed to auto-determine port for '%s'", service) 24 | } 25 | port = p 26 | } 27 | 28 | prefix := fmt.Sprintf("net.udp.%s.%d.", service, port) 29 | 30 | // Initialize MeasurementsMap 31 | m := MeasurementsMap{ 32 | prefix + "success": 0, 33 | } 34 | 35 | // Start measuring execution time 36 | started := time.Now() 37 | // Calculate execution time in the end 38 | defer func() { 39 | m[prefix+"totalTimeSpent_s"] = time.Since(started).Seconds() 40 | }() 41 | 42 | checkTimeout := secToDuration(fm.Config.NetUDPTimeout) 43 | 44 | addr := fmt.Sprintf("%s:%d", hostname, port) 45 | 46 | // Open connection to the specified addr 47 | conn, err := net.DialTimeout("udp", addr, checkTimeout) 48 | m[prefix+"connectTime_s"] = time.Since(started).Seconds() 49 | if err != nil { 50 | return m, err 51 | } 52 | defer conn.Close() 53 | 54 | err = conn.SetDeadline(time.Now().Add(checkTimeout)) 55 | if err != nil { 56 | return m, fmt.Errorf("can't set UDP conn timeout: %s", err.Error()) 57 | } 58 | 59 | // Execute the check 60 | err = executeUDPServiceCheck(conn.(*net.UDPConn), checkTimeout, service, hostname) 61 | if err != nil { 62 | return m, fmt.Errorf("failed to verify '%s' service on %d port: %s", service, port, err.Error()) 63 | } 64 | 65 | // Mark check as successful 66 | m[prefix+"success"] = 1 67 | 68 | return m, nil 69 | } 70 | 71 | // executeUDPServiceCheck executes a check based on the passed protocol name on the given connection 72 | func executeUDPServiceCheck(conn *net.UDPConn, udpTimeout time.Duration, service, hostname string) error { 73 | var err error 74 | switch service { 75 | case "sip": 76 | err = checkSIP(conn, hostname, udpTimeout) 77 | case "iax2": 78 | err = checkIAX2(conn, udpTimeout) 79 | case "dns": 80 | // minimal DNS test just verifies connection is established 81 | case "udp": 82 | // In the previous call to net.Dial the test basically already happened while establishing the connection 83 | // so we don't have to do anything additional here. 84 | default: 85 | err = fmt.Errorf("unknown service '%s'", service) 86 | } 87 | 88 | return err 89 | } 90 | 91 | func checkSIP(conn *net.UDPConn, hostname string, timeout time.Duration) error { 92 | const magicCookie = "z9hG4bK" // See: https://tools.ietf.org/html/rfc3261#section-8.1.1.7 93 | 94 | requestTemplateText := `OPTIONS sip:{{.FromUser}}@{{.Domain}} SIP/2.0 95 | Via: SIP/2.0/UDP {{.ContactDomain}}:{{.LPort}};branch={{.Branch}} 96 | From: {{.FromName}} ;tag=0c26cd11 97 | To: {{.FromName}} 98 | Contact: 99 | Call-ID: {{.CallId}} 100 | CSeq: 1 OPTIONS 101 | User-Agent: {{.UserAgent}} 102 | Max-Forwards: 70 103 | Allow: INVITE,ACK,CANCEL,BYE,NOTIFY,REFER,OPTIONS,INFO,SUBSCRIBE,UPDATE,PRACK,MESSAGE 104 | Content-Length: 0 105 | 106 | ` 107 | requestTemplateText = strings.ReplaceAll(requestTemplateText, "\n", "\r\n") 108 | requestTpl, err := template.New("request").Parse(requestTemplateText) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | localAddr := conn.LocalAddr().(*net.UDPAddr) 114 | 115 | params := map[string]string{ 116 | "FromUser": "100", 117 | "Domain": hostname, 118 | "ContactDomain": "1.1.1.1", 119 | "LPort": strconv.Itoa(localAddr.Port), 120 | "Branch": magicCookie + utils.RandomizedStr(64), 121 | "FromName": "", 122 | "ToUser": "100", 123 | "CallId": utils.RandomizedStr(32), 124 | "UserAgent": "frontman", 125 | } 126 | requestBuf := bytes.NewBuffer([]byte{}) 127 | err = requestTpl.Execute(requestBuf, params) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | _ = conn.SetWriteDeadline(time.Now().Add(timeout)) 133 | _, err = io.Copy(conn, requestBuf) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | var response = make([]byte, 1024) 139 | _ = conn.SetReadDeadline(time.Now().Add(timeout)) 140 | n, _, err := conn.ReadFrom(response) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | expected := []byte("SIP/2") 146 | if !bytes.HasPrefix(response, expected) { 147 | return fmt.Errorf("invalid response: expected to start with '%s' but got '%s'", string(expected), string(response[0:n])) 148 | } 149 | 150 | return nil 151 | } 152 | 153 | func checkIAX2(conn *net.UDPConn, timeout time.Duration) error { 154 | pokePacket := iax.GetPokeFramePacket() 155 | 156 | _ = conn.SetWriteDeadline(time.Now().Add(timeout)) 157 | _, err := conn.Write(pokePacket) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | _ = conn.SetReadDeadline(time.Now().Add(timeout)) 163 | 164 | var b = make([]byte, 128) 165 | _, err = conn.Read(b) 166 | if err != nil { 167 | return err 168 | } 169 | 170 | if !iax.IsPongResponse(b) { 171 | return fmt.Errorf("invalid response to POKE request. Received bytes: %v", b) 172 | } 173 | 174 | // send ACK to close connection 175 | // new version of Asterisk do not require this, 176 | // but old will resend PONG packets 177 | ackPacket := iax.GetAckFramePacket() 178 | _ = conn.SetWriteDeadline(time.Now().Add(timeout)) 179 | _, _ = conn.Write(ackPacket) 180 | 181 | return nil 182 | } 183 | -------------------------------------------------------------------------------- /example.config.toml: -------------------------------------------------------------------------------- 1 | # This is example config 2 | 3 | # Name of the Frontman 4 | # Used to identify group measurements if multiple frontmen run in grouped-mode (ask_neighbor) 5 | node_name = "Frontman" 6 | 7 | sleep = 5.0 # delay before starting a new round of checks in seconds; number must contains decimal point 8 | pid = "/tmp/frontman.pid" # pid file location 9 | stats_file = "/tmp/frontman.stats" 10 | 11 | # Logging 12 | log = "/var/log/frontman/frontman.log" # log file location 13 | log_syslog = "" # ""(don't use syslog), "local"(use local unix socket) or "udp://localhost:554 14 | log_level = "info" # "debug", "info", "error" verbose level; can be overriden with -v flag 15 | 16 | # ICMP pings 17 | icmp_timeout = 0.5 # ICMP ping timeout in seconds; number must contains decimal point 18 | 19 | # TCP checks 20 | net_tcp_timeout = 2.0 # TCP timeout in seconds; number must contains decimal point 21 | 22 | # UDP checks 23 | net_udp_timeout = 1.5 # UDP timeout in seconds; number must contains decimal point 24 | 25 | # Web checks 26 | http_tcp_timeout = 15.0 # HTTP timeout in seconds; number must contains decimal point 27 | max_redirects = 3 # Max number of HTTP redirects to follow 28 | ignore_ssl_errors = false # Ignore SSL errors (e.g. self-signed or expired certificate) 29 | ssl_cert_expiry_threshold = 7 # Min days remain on the SSL cert to pass the check 30 | 31 | # Input and results 32 | io_mode = "http" # "file" or "http" – where frontman gets checks to perform and post results, can be overriden with -i and -o flag 33 | hub_url = "" # requires io_mode to be "http" 34 | hub_user = "" # requires io_mode to be "http" 35 | hub_password = "" # requires io_mode to be "http" 36 | hub_proxy = "" # HTTP proxy to use with HUB, requires io_mode to be "http" 37 | hub_proxy_user = "" # requires hub_proxy to be set 38 | hub_proxy_password = "" # requires hub_proxy_user to be set 39 | hub_request_timeout = 10 40 | 41 | # System 42 | # host_info of frontman machine will be sent to hub 43 | # default ['uname','os_kernel','os_family','os_arch','cpu_model','fqdn','hostname','memory_total_B'] 44 | host_info = ['uname','os_kernel','os_family','os_arch','cpu_model','fqdn','hostname','memory_total_B'] 45 | 46 | # 47 | # Frontman can perform health checks before executing all other checks. 48 | # This is useful to confirm a stable internet connection to avoid false alerts due to network outages 49 | # The health check is performed every time a new check round starts according to the sleep interval. 50 | # If the health check fails, the round is skipped and no checks are performed. 51 | # 52 | [health_checks] 53 | # Ping all hosts of the list. Only if frontman gets a positive answer form all of them, frontman continues. 54 | # Only 0% packet loss is considered as a positive check result. Pings are performed in parallel. 55 | reference_ping_hosts = ['8.8.8.8','1.1.1.1','8.8.4.4'] 56 | # Maximum time (seconds) to wait for the response. 57 | reference_ping_timeout = 0.5 58 | # Number of request packets to send to each host. 59 | reference_ping_count = 1 60 | 61 | # Frontman can execute a failed check on other frontmen - ideally on different locations - 62 | # to confirm the check fails everywhere. 63 | # Only if the check fails on all of them it's considered as failed and sent back to the hub. 64 | # If the check succeeds on one frontman this check result is sent back 65 | # Requires the HTTP listener enabled on the foreign frontman 66 | # Example: 67 | # [nodes] 68 | # [nodes.1] 69 | # url = "https://frontman-1.example.com:9955" 70 | # username = "frontman" 71 | # password = "secret" 72 | # verify_ssl = true 73 | 74 | # Node configuration 75 | [node] 76 | # Set the maximum time in seconds frontman should spend trying to connect a node 77 | node_timeout = 3.0 78 | 79 | # Cache errors for N seconds. If the connection to a node fails for whatever reason, this node is not asked again, until the error cache has expired. 80 | node_cache_errors = 10.0 81 | 82 | # Do not forward failed checks to the foreign node(s) if the message contains one of the following regular expresions. 83 | # Matching is case insensitive. 84 | forward_except = [ 85 | 'bad status code', 86 | 'certificate.*(expire|unknown)', 87 | '(tls|ssl) (error|failed|handshake)', 88 | 'service.*support (ssl|tls)', 89 | 'failed to verify .* service', 90 | 'connection.*refused', 91 | 'no such host', 92 | 'x509', 93 | 'pattern.*extraxcted text' 94 | ] 95 | 96 | # Log all checks forwarded to foreign node(s). 97 | # The log contains the check ID, the check type, and the message of the local check result. 98 | forward_log = "/tmp/frontman-forward.log" 99 | 100 | [http_listener] 101 | # HTTP Listener 102 | # Perform checks requested via HTTP POST requests on '/check' 103 | # Examples: 104 | # http_listen = "http://0.0.0.0:9090" # for unencrypted http connections 105 | # http_listen = "https://0.0.0.0:8443" # for encrypted https connections 106 | # execute "sudo setcap cap_net_bind_service=+ep /usr/bin/frontman" to use ports < 1024 107 | # Executing SNMP check through the HTTP Listener is not supported. 108 | http_listen = "" 109 | 110 | # Private key for https connections 111 | http_tls_key = "" 112 | 113 | # Certificate for https connections 114 | http_tls_cert = "" 115 | 116 | # Username for the http basic authentication. If omitted authentication is disabled 117 | http_auth_user = "" 118 | 119 | # Password for the http basic authentication. 120 | http_auth_password = "" 121 | 122 | # Log http requests. On windows slash must be escaped like "C:\\access.log" 123 | http_access_log = "" 124 | 125 | # Control how frontman installs self-updates. Windows-only 126 | [self_update] 127 | enabled = true # Set to false to disable self-updates 128 | check_interval = 21600 # Frontman will check for new versions every N seconds 129 | -------------------------------------------------------------------------------- /webcheck_test.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestWebCheck(t *testing.T) { 13 | cfg, _ := HandleAllConfigSetup(DefaultCfgPath) 14 | cfg.HTTPCheckTimeout = 10.0 15 | cfg.Sleep = 10 16 | fm := helperCreateFrontman(t, cfg) 17 | input := &Input{ 18 | WebChecks: []WebCheck{{ 19 | UUID: "webcheck1", 20 | Check: WebCheckData{ 21 | Timeout: 1.0, 22 | URL: "https://www.google.com", 23 | Method: "get", 24 | ExpectedHTTPStatus: 200, 25 | }, 26 | }}, 27 | } 28 | fm.processInput(input.asChecks(), true) 29 | res := <-fm.resultsChan 30 | require.Equal(t, nil, res.Message) 31 | require.Equal(t, 1, res.Measurements["http.get.success"]) 32 | } 33 | 34 | func TestWebCheckHeaders(t *testing.T) { 35 | // verifies that extra headers are being set in http requests 36 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 37 | w.Header().Set("Content-Type", "text/html") 38 | w.WriteHeader(http.StatusOK) 39 | 40 | assert.Equal(t, "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", r.Header.Get("Authorization")) 41 | assert.Equal(t, "no-cache", r.Header.Get("Cache-Control")) 42 | })) 43 | defer ts.Close() 44 | 45 | cfg, _ := HandleAllConfigSetup(DefaultCfgPath) 46 | cfg.HTTPCheckTimeout = 10.0 47 | cfg.Sleep = 10 48 | fm := helperCreateFrontman(t, cfg) 49 | input := &Input{ 50 | WebChecks: []WebCheck{{ 51 | UUID: "webcheck1", 52 | Check: WebCheckData{ 53 | Timeout: 1.0, 54 | URL: ts.URL, 55 | Method: "get", 56 | ExpectedHTTPStatus: 200, 57 | Headers: map[string]string{ 58 | "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", 59 | "cache-control": "no-cache", 60 | }, 61 | }, 62 | }}, 63 | } 64 | fm.processInput(input.asChecks(), true) 65 | res := <-fm.resultsChan 66 | require.Equal(t, nil, res.Message) 67 | require.Equal(t, 1, res.Measurements["http.get.success"]) 68 | } 69 | 70 | func TestNormalizeURLPort(t *testing.T) { 71 | var urls = []struct { 72 | input string 73 | expected string 74 | }{ 75 | {"http://www.google.com:80/en/", "http://www.google.com/en/"}, 76 | {"https://www.google.com:443/en/", "https://www.google.com/en/"}, 77 | {"https://www.google.com:5555/en/", "https://www.google.com:5555/en/"}, 78 | } 79 | 80 | for _, u := range urls { 81 | url, err := normalizeURLPort(u.input) 82 | assert.Equal(t, u.expected, url) 83 | assert.Equal(t, nil, err) 84 | } 85 | } 86 | 87 | func TestWebCheckPresentTextSuccess(t *testing.T) { 88 | cfg, _ := HandleAllConfigSetup(DefaultCfgPath) 89 | cfg.HTTPCheckTimeout = 10.0 90 | cfg.Sleep = 10 91 | fm := helperCreateFrontman(t, cfg) 92 | input := &Input{ 93 | WebChecks: []WebCheck{{ 94 | UUID: "webcheck1", 95 | Check: WebCheckData{ 96 | Timeout: 2.0, 97 | URL: "https://www.google.com", 98 | Method: "get", 99 | ExpectedHTTPStatus: 200, 100 | SearchHTMLSource: true, 101 | ExpectedPattern: "Google", 102 | ExpectedPatternPresence: "present", 103 | }, 104 | }}, 105 | } 106 | fm.processInput(input.asChecks(), true) 107 | res := <-fm.resultsChan 108 | require.Equal(t, nil, res.Message) 109 | require.Equal(t, 1, res.Measurements["http.get.success"]) 110 | } 111 | 112 | func TestWebCheckPresentTextFail(t *testing.T) { 113 | cfg, _ := HandleAllConfigSetup(DefaultCfgPath) 114 | cfg.HTTPCheckTimeout = 10.0 115 | cfg.Sleep = 10 116 | fm := helperCreateFrontman(t, cfg) 117 | input := &Input{ 118 | WebChecks: []WebCheck{{ 119 | UUID: "webcheck1", 120 | Check: WebCheckData{ 121 | Timeout: 2.0, 122 | URL: "https://www.google.com", 123 | Method: "get", 124 | ExpectedHTTPStatus: 200, 125 | SearchHTMLSource: false, 126 | ExpectedPattern: "yahoo rules", 127 | ExpectedPatternPresence: "present", 128 | }, 129 | }}, 130 | } 131 | fm.processInput(input.asChecks(), true) 132 | res := <-fm.resultsChan 133 | require.Equal(t, "pattern expected to be present 'yahoo rules' not found in the extracted text", res.Message) 134 | } 135 | 136 | func TestWebCheckAbsentTextSuccess(t *testing.T) { 137 | cfg, _ := HandleAllConfigSetup(DefaultCfgPath) 138 | cfg.HTTPCheckTimeout = 10.0 139 | cfg.Sleep = 10 140 | fm := helperCreateFrontman(t, cfg) 141 | input := &Input{ 142 | WebChecks: []WebCheck{{ 143 | UUID: "webcheck1", 144 | Check: WebCheckData{ 145 | Timeout: 2.0, 146 | URL: "https://www.google.com", 147 | Method: "get", 148 | ExpectedHTTPStatus: 200, 149 | SearchHTMLSource: true, 150 | ExpectedPattern: "Yahoo", 151 | ExpectedPatternPresence: "absent", 152 | }, 153 | }}, 154 | } 155 | fm.processInput(input.asChecks(), true) 156 | res := <-fm.resultsChan 157 | require.Equal(t, nil, res.Message) 158 | require.Equal(t, 1, res.Measurements["http.get.success"]) 159 | } 160 | 161 | func TestWebCheckAbsentTextFail(t *testing.T) { 162 | cfg, _ := HandleAllConfigSetup(DefaultCfgPath) 163 | cfg.HTTPCheckTimeout = 10.0 164 | cfg.Sleep = 10 165 | fm := helperCreateFrontman(t, cfg) 166 | input := &Input{ 167 | WebChecks: []WebCheck{{ 168 | UUID: "webcheck1", 169 | Check: WebCheckData{ 170 | Timeout: 2.0, 171 | URL: "https://www.google.com", 172 | Method: "get", 173 | ExpectedHTTPStatus: 200, 174 | SearchHTMLSource: false, 175 | ExpectedPattern: "Google", 176 | ExpectedPatternPresence: "absent", 177 | }, 178 | }}, 179 | } 180 | fm.processInput(input.asChecks(), true) 181 | res := <-fm.resultsChan 182 | require.Equal(t, "pattern expected to be absent 'Google' found in the extracted text", res.Message) 183 | } 184 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | # default concurrency is a available CPU number 3 | concurrency: 4 4 | 5 | # timeout for analysis, e.g. 30s, 5m, default is 1m 6 | deadline: 1m 7 | 8 | # exit code when at least one issue was found, default is 1 9 | issues-exit-code: 1 10 | 11 | # include test files or not, default is true 12 | tests: true 13 | 14 | output: 15 | # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number" 16 | format: colored-line-number 17 | 18 | # print lines of code with issue, default is true 19 | print-issued-lines: true 20 | 21 | # print linter name in the end of issue text, default is true 22 | print-linter-name: true 23 | 24 | # all available settings of specific linters 25 | linters-settings: 26 | errcheck: 27 | # report about not checking of errors in type assetions: `a := b.(MyStruct)`; 28 | # default is false: such cases aren't reported by default. 29 | check-type-assertions: false 30 | 31 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 32 | # default is false: such cases aren't reported by default. 33 | check-blank: false 34 | govet: 35 | # report about shadowed variables 36 | check-shadowing: true 37 | golint: 38 | # minimal confidence for issues, default is 0.8 39 | min-confidence: 0.8 40 | gofmt: 41 | # simplify code: gofmt with `-s` option, true by default 42 | simplify: true 43 | goimports: 44 | # put imports beginning with prefix after 3rd-party packages; 45 | # it's a comma-separated list of prefixes 46 | local-prefixes: github.com/cloudradar-monitoring/cagent 47 | gocyclo: 48 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 49 | min-complexity: 25 50 | maligned: 51 | # print struct with more effective memory layout or not, false by default 52 | suggest-new: true 53 | dupl: 54 | # tokens count to trigger issue, 150 by default 55 | threshold: 100 56 | goconst: 57 | # minimal length of string constant, 3 by default 58 | min-len: 3 59 | # minimal occurrences count to trigger, 3 by default 60 | min-occurrences: 3 61 | depguard: 62 | list-type: blacklist 63 | include-go-root: false 64 | packages: 65 | misspell: 66 | # Correct spellings using locale preferences for US or UK. 67 | # Default is to use a neutral variety of English. 68 | # Setting locale to US will correct the British spelling of 'colour' to 'color'. 69 | locale: US 70 | ignore-words: 71 | - utilisation # can't be fixed due to back compatibility issues. 72 | lll: 73 | # max line length, lines longer will be reported. Default is 120. 74 | # '\t' is counted as 1 character by default, and can be changed with the tab-width option 75 | line-length: 120 76 | # tab width in spaces. Default to 1. 77 | tab-width: 1 78 | unused: 79 | # treat code as a program (not a library) and report unused exported identifiers; default is false. 80 | # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: 81 | # if it's called for subdir of a project it can't find funcs usages. All text editor integrations 82 | # with golangci-lint call it on a directory with the changed file. 83 | check-exported: false 84 | unparam: 85 | # call graph construction algorithm (cha, rta). In general, use cha for libraries, 86 | # and rta for programs with main packages. Default is cha. 87 | algo: cha 88 | 89 | # Inspect exported functions, default is false. Set to true if no external program/library imports your code. 90 | # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: 91 | # if it's called for subdir of a project it can't find external interfaces. All text editor integrations 92 | # with golangci-lint call it on a directory with the changed file. 93 | check-exported: false 94 | nakedret: 95 | # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 96 | max-func-lines: 30 97 | prealloc: 98 | # XXX: we don't recommend using this linter before doing performance profiling. 99 | # For most programs usage of prealloc will be a premature optimization. 100 | 101 | # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. 102 | # True by default. 103 | simple: true 104 | range-loops: true # Report preallocation suggestions on range loops, true by default 105 | for-loops: false # Report preallocation suggestions on for loops, false by default 106 | 107 | 108 | linters: 109 | enable: 110 | - goimports 111 | - golint 112 | - gosimple 113 | - structcheck 114 | - deadcode 115 | - staticcheck 116 | - errcheck 117 | - unused 118 | - gosec 119 | - dupl 120 | - gocyclo 121 | - misspell 122 | - unparam 123 | enable-all: false 124 | disable: 125 | disable-all: true 126 | presets: 127 | fast: false 128 | 129 | # issues: 130 | # # List of regexps of issue texts to exclude, empty list by default. 131 | # # But independently from this option we use default exclude patterns, 132 | # # it can be disabled by `exclude-use-default: false`. To list all 133 | # # excluded by default patterns execute `golangci-lint run --help` 134 | # exclude: 135 | # - abcdef 136 | 137 | # # Independently from option `exclude` we use default exclude patterns, 138 | # # it can be disabled by this option. To list all 139 | # # excluded by default patterns execute `golangci-lint run --help`. 140 | # # Default value for this option is true. 141 | # exclude-use-default: false 142 | 143 | # # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 144 | # max-per-linter: 0 145 | 146 | # # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 147 | # max-same-issues: 0 148 | 149 | # # Show only new issues: if there are unstaged changes or untracked files, 150 | # # only those changes are analyzed, else only changes in HEAD~ are analyzed. 151 | # # It's a super-useful option for integration of golangci-lint into existing 152 | # # large codebase. It's not practical to fix all existing issues at the moment 153 | # # of integration: much better don't allow issues in new code. 154 | # # Default is false. 155 | # new: false 156 | 157 | new: true 158 | # # Show only new issues created after git revision `REV` 159 | # new-from-rev: REV 160 | 161 | # # Show only new issues created in git patch with set file path. 162 | # new-from-patch: path/to/patch/file 163 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | 4 | orbs: 5 | ms-teams: cloudradar-monitoring/ms-teams@0.0.1 6 | 7 | docker_job_setup: &docker_job 8 | docker: 9 | - image: cloudradario/go-build:0.0.19 10 | working_directory: /go/src/github.com/cloudradar-monitoring/frontman 11 | 12 | attach_workspace: &workspace 13 | attach_workspace: 14 | at: /go/src/github.com/cloudradar-monitoring 15 | 16 | common_workflow_setup: &common_workflow 17 | context: cloudradar 18 | post-steps: 19 | - ms-teams/report: 20 | only_on_fail: true 21 | webhook_url: $MS_TEAMS_WEBHOOK_URL 22 | 23 | jobs: 24 | get-source: 25 | <<: *docker_job 26 | steps: 27 | - checkout 28 | - persist_to_workspace: 29 | root: /go/src/github.com/cloudradar-monitoring 30 | paths: 31 | - frontman 32 | 33 | test: 34 | <<: *docker_job 35 | steps: 36 | - <<: *workspace 37 | - run: go test -v -short -race ./... 38 | 39 | test-goreleaser: 40 | <<: *docker_job 41 | steps: 42 | - <<: *workspace 43 | - run: make goreleaser-snapshot 44 | 45 | build-packages: 46 | <<: *docker_job 47 | parameters: 48 | release_mode: 49 | type: string 50 | environment: 51 | RELEASE_MODE: << parameters.release_mode >> 52 | steps: 53 | - <<: *workspace 54 | - run: 55 | name: Build binaries and pack them 56 | command: GORELEASER_CURRENT_TAG=${CIRCLE_TAG} make goreleaser-rm-dist 57 | - run: 58 | name: Build Synology packages 59 | shell: /bin/bash 60 | command: | 61 | cd synology-spk && ./create_spk.sh ${CIRCLE_TAG} 62 | - run: 63 | name: Scan build artifacts with VirusTotal 64 | command: | 65 | go get github.com/cloudradar-monitoring/virustotal-scan && 66 | virustotal-scan --verbose --ignore Cylance,Jiangmin,Ikarus,MaxSecure,Microsoft --apikey ${VIRUSTOTAL_TOKEN} --file dist/frontman_${CIRCLE_TAG}_Windows_x86_64.zip 67 | - persist_to_workspace: 68 | root: /go/src/github.com/cloudradar-monitoring 69 | paths: 70 | - frontman 71 | 72 | publish-packages: 73 | <<: *docker_job 74 | parameters: 75 | release_mode: 76 | type: string 77 | environment: 78 | RELEASE_MODE: << parameters.release_mode >> 79 | steps: 80 | - <<: *workspace 81 | - add_ssh_keys: 82 | fingerprints: 83 | - "53:d2:08:dc:1a:4e:9e:29:00:d4:ba:1e:b7:5d:16:25" 84 | - "53:8f:20:fd:32:2e:af:95:4f:3e:2b:05:2d:81:34:b1" 85 | - run: 86 | name: Publish packages 87 | command: .circleci/publish-packages.sh 88 | - run: 89 | name: Cleanup in case something went wrong 90 | command: .circleci/unpublish-packages.sh 91 | when: on_fail 92 | 93 | 94 | build-docker: 95 | <<: *docker_job 96 | steps: 97 | - <<: *workspace 98 | - setup_remote_docker 99 | - run: 100 | name: Install Docker client 101 | command: | 102 | set -x 103 | VER="18.06.3-ce" 104 | curl -L -o /tmp/docker-$VER.tgz https://download.docker.com/linux/static/stable/x86_64/docker-$VER.tgz 105 | tar -xz -C /tmp -f /tmp/docker-$VER.tgz 106 | mv /tmp/docker/* /usr/bin 107 | - run: | 108 | docker login --username ${DOCKERHUB_USER} --password ${DOCKERHUB_PASS} 109 | docker build --build-arg FRONTMAN_VERSION=${CIRCLE_TAG} -t cloudradario/frontman:${CIRCLE_TAG} . 110 | docker push cloudradario/frontman:${CIRCLE_TAG} 111 | 112 | workflows: 113 | version: 2 114 | test-on-commit: 115 | jobs: 116 | - get-source: 117 | <<: *common_workflow 118 | filters: 119 | tags: 120 | ignore: /.*/ 121 | - test: 122 | <<: *common_workflow 123 | requires: 124 | - get-source 125 | filters: 126 | tags: 127 | ignore: /.*/ 128 | - test-goreleaser: 129 | <<: *common_workflow 130 | requires: 131 | - get-source 132 | filters: 133 | tags: 134 | ignore: /.*/ 135 | 136 | release: 137 | jobs: 138 | - get-source: 139 | <<: *common_workflow 140 | filters: 141 | tags: 142 | only: /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/ 143 | branches: 144 | ignore: /.*/ 145 | - test: 146 | <<: *common_workflow 147 | requires: 148 | - get-source 149 | filters: 150 | tags: 151 | only: /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/ 152 | branches: 153 | ignore: /.*/ 154 | - build-packages: 155 | <<: *common_workflow 156 | name: build-packages-release-candidate 157 | release_mode: release-candidate 158 | requires: 159 | - test 160 | filters: 161 | tags: 162 | only: /^(0|[1-9]\d*)\.([1-9]\d*[13579]|[13579])\.(0|[1-9]\d*)$/ 163 | branches: 164 | ignore: /.*/ 165 | - build-packages: 166 | <<: *common_workflow 167 | name: build-packages-stable 168 | release_mode: stable 169 | requires: 170 | - test 171 | filters: 172 | tags: 173 | only: /^(0|[1-9]\d*)\.([1-9]\d*[02468]|[02468])\.(0|[1-9]\d*)$/ 174 | branches: 175 | only: 176 | - master 177 | - publish-packages: 178 | <<: *common_workflow 179 | name: publish-packages-release-candidate 180 | release_mode: release-candidate 181 | requires: 182 | - build-packages-release-candidate 183 | filters: 184 | tags: 185 | only: /^(0|[1-9]\d*)\.([1-9]\d*[13579]|[13579])\.(0|[1-9]\d*)$/ 186 | branches: 187 | ignore: /.*/ 188 | - publish-packages: 189 | <<: *common_workflow 190 | name: publish-packages-stable 191 | release_mode: stable 192 | requires: 193 | - build-packages-stable 194 | filters: 195 | tags: 196 | only: /^(0|[1-9]\d*)\.([1-9]\d*[02468]|[02468])\.(0|[1-9]\d*)$/ 197 | branches: 198 | only: 199 | - master 200 | - build-docker: 201 | <<: *common_workflow 202 | requires: 203 | - publish-packages-stable 204 | filters: 205 | tags: 206 | only: /^(0|[1-9]\d*)\.([1-9]\d*[02468]|[02468])\.(0|[1-9]\d*)$/ 207 | branches: 208 | only: 209 | - master 210 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= 2 | github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= 3 | github.com/cloudradar-monitoring/selfupdate v0.0.0-20200615195818-3bc6d247a637 h1:RJiepFT4AMVaWUe8UAa0R1HgJlnMmdQ871yXpGVTMXc= 4 | github.com/cloudradar-monitoring/selfupdate v0.0.0-20200615195818-3bc6d247a637/go.mod h1:0uKPaZjO2Xoh/uY6SKlPsSnw4uLFXIlOnjqJBnE00CA= 5 | github.com/cloudradar-monitoring/toml v0.4.3-0.20190904091934-b07890c4335d h1:JgIl3x2y5BpFb/oHhVow+e++bUucYv269FXLfquGNSw= 6 | github.com/cloudradar-monitoring/toml v0.4.3-0.20190904091934-b07890c4335d/go.mod h1:7F3c4192Vjhyw6dPkqCPhIKlD/XAOT5iV+3Y5vS7ejk= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= 11 | github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 12 | github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= 13 | github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= 14 | github.com/go-ping/ping v0.0.0-20201022122018-3977ed72668a h1:O9xspHB2yrvKfMQ1m6OQhqe37i5yvg0dXAYMuAjugmM= 15 | github.com/go-ping/ping v0.0.0-20201022122018-3977ed72668a/go.mod h1:35JbSyV/BYqHwwRA6Zr1uVDm1637YlNOU61wI797NPI= 16 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 17 | github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= 18 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 19 | github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= 20 | github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= 21 | github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= 22 | github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 23 | github.com/kardianos/service v1.2.0 h1:bGuZ/epo3vrt8IPC7mnKQolqFeYJb7Cs8Rk4PSOBB/g= 24 | github.com/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= 25 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 26 | github.com/lxn/walk v0.0.0-20190515104301-6cf0bf1359a5 h1:51pEh8Uk7stl19omqzMOGWBavA8w1Cs2Bf15yMEENKo= 27 | github.com/lxn/walk v0.0.0-20190515104301-6cf0bf1359a5/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= 28 | github.com/lxn/win v0.0.0-20190514122436-6f00d814e89c h1:RmJqAqztNMamrAAP8zti9PbuD4D897Wa//LHSAh9Vow= 29 | github.com/lxn/win v0.0.0-20190514122436-6f00d814e89c/go.mod h1:oO6+4g3P1GcPAG7LPffwn8Ye0cxW0goh0sUZ6+lRFPs= 30 | github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA= 31 | github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= 32 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= 33 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= 34 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 35 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 36 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 37 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 38 | github.com/shirou/gopsutil v2.20.9+incompatible h1:msXs2frUV+O/JLva9EDLpuJ84PrFsdCTCQex8PUdtkQ= 39 | github.com/shirou/gopsutil v2.20.9+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 40 | github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U= 41 | github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= 42 | github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= 43 | github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= 44 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 45 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 46 | github.com/soniah/gosnmp v1.21.1-0.20190510081145-1b12be15031c h1:4y+03NBBvzzIicHm8yvdH9L2bz4hpThNgft+kVyILEk= 47 | github.com/soniah/gosnmp v1.21.1-0.20190510081145-1b12be15031c/go.mod h1:DuEpAS0az51+DyVBQwITDsoq4++e3LTNckp2GoasF2I= 48 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 49 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 50 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 51 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 52 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 53 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 54 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 56 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 57 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 58 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 59 | golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 60 | golang.org/x/net v0.0.0-20201029055024-942e2f445f3c h1:rpcgRPA7OvNEOdprt2Wx8/Re2cBTd8NPo/lvo3AyMqk= 61 | golang.org/x/net v0.0.0-20201029055024-942e2f445f3c/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 62 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 64 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 67 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20201214095126-aec9a390925b h1:tv7/y4pd+sR8bcNb2D6o7BNU6zjWm0VjQLac+w7fNNM= 72 | golang.org/x/sys v0.0.0-20201214095126-aec9a390925b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 74 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 75 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 76 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 77 | gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= 78 | gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= 79 | gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= 80 | gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= 81 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 82 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 83 | gopkg.in/ldap.v3 v3.0.3 h1:YKRHW/2sIl05JsCtx/5ZuUueFuJyoj/6+DGXe3wp6ro= 84 | gopkg.in/ldap.v3 v3.0.3/go.mod h1:oxD7NyBuxchC+SgJDE1Q5Od05eGt29SDQVBmV+HYbzw= 85 | gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 h1:MZF6J7CV6s/h0HBkfqebrYfKCVEo5iN+wzE4QhV3Evo= 86 | gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2/go.mod h1:s1Sn2yZos05Qfs7NKt867Xe18emOmtsO3eAKbDaon0o= 87 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 88 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 89 | -------------------------------------------------------------------------------- /pkg/winui/winui_multipage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Walk Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build windows 6 | 7 | package winui 8 | 9 | import ( 10 | "github.com/lxn/walk" 11 | decl "github.com/lxn/walk/declarative" 12 | ) 13 | 14 | type MultiPageMainWindowConfig struct { 15 | Name string 16 | Enabled decl.Property 17 | Visible decl.Property 18 | Font decl.Font 19 | MinSize decl.Size 20 | MaxSize decl.Size 21 | ContextMenuItems []decl.MenuItem 22 | OnKeyDown walk.KeyEventHandler 23 | OnKeyPress walk.KeyEventHandler 24 | OnKeyUp walk.KeyEventHandler 25 | OnMouseDown walk.MouseEventHandler 26 | OnMouseMove walk.MouseEventHandler 27 | OnMouseUp walk.MouseEventHandler 28 | OnSizeChanged walk.EventHandler 29 | OnCurrentPageChanged walk.EventHandler 30 | Title string 31 | Size decl.Size 32 | MenuItems []decl.MenuItem 33 | ToolBar decl.ToolBar 34 | PageCfgs []PageConfig 35 | StatusBarItems []decl.StatusBarItem 36 | } 37 | 38 | type PageConfig struct { 39 | Title string 40 | Image string 41 | NewPage PageFactoryFunc 42 | } 43 | 44 | type PageFactoryFunc func(parent walk.Container) (Page, error) 45 | 46 | type Page interface { 47 | // Provided by Walk 48 | walk.Container 49 | Parent() walk.Container 50 | SetParent(parent walk.Container) error 51 | } 52 | 53 | type MultiPageMainWindow struct { 54 | *walk.MainWindow 55 | navTB *walk.ToolBar 56 | pageCom *walk.Composite 57 | action2NewPage map[*walk.Action]PageFactoryFunc 58 | pageActions []*walk.Action 59 | currentAction *walk.Action 60 | currentPage Page 61 | currentPageChangedPublisher walk.EventPublisher 62 | } 63 | 64 | func NewMultiPageMainWindow(cfg *MultiPageMainWindowConfig) (*MultiPageMainWindow, error) { 65 | mpmw := &MultiPageMainWindow{ 66 | action2NewPage: make(map[*walk.Action]PageFactoryFunc), 67 | } 68 | 69 | if err := (decl.MainWindow{ 70 | AssignTo: &mpmw.MainWindow, 71 | Name: cfg.Name, 72 | Title: cfg.Title, 73 | Enabled: cfg.Enabled, 74 | Visible: cfg.Visible, 75 | Font: cfg.Font, 76 | MinSize: cfg.MinSize, 77 | MaxSize: cfg.MaxSize, 78 | MenuItems: cfg.MenuItems, 79 | ToolBar: cfg.ToolBar, 80 | ContextMenuItems: cfg.ContextMenuItems, 81 | OnKeyDown: cfg.OnKeyDown, 82 | OnKeyPress: cfg.OnKeyPress, 83 | OnKeyUp: cfg.OnKeyUp, 84 | OnMouseDown: cfg.OnMouseDown, 85 | OnMouseMove: cfg.OnMouseMove, 86 | OnMouseUp: cfg.OnMouseUp, 87 | OnSizeChanged: cfg.OnSizeChanged, 88 | Layout: decl.HBox{MarginsZero: true, SpacingZero: true}, 89 | Children: []decl.Widget{ 90 | decl.ScrollView{ 91 | HorizontalFixed: true, 92 | Layout: decl.VBox{MarginsZero: true}, 93 | Children: []decl.Widget{ 94 | decl.Composite{ 95 | Layout: decl.VBox{MarginsZero: true}, 96 | Children: []decl.Widget{ 97 | PagesToolBar{ 98 | ToolBar: decl.ToolBar{ 99 | AssignTo: &mpmw.navTB, 100 | Orientation: decl.Vertical, 101 | ButtonStyle: decl.ToolBarButtonImageAboveText, 102 | MaxTextRows: 2, 103 | }, 104 | IconSize: walk.Size{Width: 40, Height: 40}, 105 | }, 106 | }, 107 | }, 108 | }, 109 | }, 110 | decl.Composite{ 111 | AssignTo: &mpmw.pageCom, 112 | Name: "pageCom", 113 | Layout: decl.HBox{MarginsZero: true, SpacingZero: true}, 114 | }, 115 | }, 116 | StatusBarItems: cfg.StatusBarItems, 117 | }).Create(); err != nil { 118 | return nil, err 119 | } 120 | 121 | succeeded := false 122 | defer func() { 123 | if !succeeded { 124 | mpmw.Dispose() 125 | } 126 | }() 127 | 128 | for _, pc := range cfg.PageCfgs { 129 | action, err := mpmw.newPageAction(pc.Title, pc.Image, pc.NewPage) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | mpmw.pageActions = append(mpmw.pageActions, action) 135 | } 136 | 137 | if err := mpmw.updateNavigationToolBar(); err != nil { 138 | return nil, err 139 | } 140 | 141 | if len(mpmw.pageActions) > 0 { 142 | if err := mpmw.setCurrentAction(mpmw.pageActions[0]); err != nil { 143 | return nil, err 144 | } 145 | } 146 | 147 | if cfg.OnCurrentPageChanged != nil { 148 | mpmw.CurrentPageChanged().Attach(cfg.OnCurrentPageChanged) 149 | } 150 | 151 | succeeded = true 152 | 153 | return mpmw, nil 154 | } 155 | 156 | func (mpmw *MultiPageMainWindow) CurrentPage() Page { 157 | return mpmw.currentPage 158 | } 159 | 160 | func (mpmw *MultiPageMainWindow) CurrentPageTitle() string { 161 | if mpmw.currentAction == nil { 162 | return "" 163 | } 164 | 165 | return mpmw.currentAction.Text() 166 | } 167 | 168 | func (mpmw *MultiPageMainWindow) CurrentPageChanged() *walk.Event { 169 | return mpmw.currentPageChangedPublisher.Event() 170 | } 171 | 172 | func (mpmw *MultiPageMainWindow) newPageAction(title, image string, newPage PageFactoryFunc) (*walk.Action, error) { 173 | img, err := walk.Resources.Bitmap(image) 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | action := walk.NewAction() 179 | action.SetCheckable(true) 180 | action.SetExclusive(true) 181 | action.SetImage(img) 182 | action.SetText(title) 183 | 184 | mpmw.action2NewPage[action] = newPage 185 | 186 | action.Triggered().Attach(func() { 187 | mpmw.setCurrentAction(action) 188 | }) 189 | 190 | return action, nil 191 | } 192 | 193 | func (mpmw *MultiPageMainWindow) setCurrentAction(action *walk.Action) error { 194 | defer func() { 195 | if !mpmw.pageCom.IsDisposed() { 196 | mpmw.pageCom.RestoreState() 197 | mpmw.pageCom.Layout().Update(false) 198 | } 199 | }() 200 | 201 | mpmw.SetFocus() 202 | 203 | if prevPage := mpmw.currentPage; prevPage != nil { 204 | mpmw.pageCom.SaveState() 205 | prevPage.SetVisible(false) 206 | prevPage.(walk.Widget).SetParent(nil) 207 | prevPage.Dispose() 208 | } 209 | 210 | newPage := mpmw.action2NewPage[action] 211 | 212 | page, err := newPage(mpmw.pageCom) 213 | if err != nil { 214 | return err 215 | } 216 | 217 | action.SetChecked(true) 218 | 219 | mpmw.currentPage = page 220 | mpmw.currentAction = action 221 | 222 | mpmw.currentPageChangedPublisher.Publish() 223 | 224 | return nil 225 | } 226 | 227 | func (mpmw *MultiPageMainWindow) updateNavigationToolBar() error { 228 | mpmw.navTB.SetSuspended(true) 229 | defer mpmw.navTB.SetSuspended(false) 230 | 231 | actions := mpmw.navTB.Actions() 232 | 233 | if err := actions.Clear(); err != nil { 234 | return err 235 | } 236 | 237 | for _, action := range mpmw.pageActions { 238 | if err := actions.Add(action); err != nil { 239 | return err 240 | } 241 | } 242 | 243 | if mpmw.currentAction != nil { 244 | if !actions.Contains(mpmw.currentAction) { 245 | for _, action := range mpmw.pageActions { 246 | if action != mpmw.currentAction { 247 | if err := mpmw.setCurrentAction(action); err != nil { 248 | return err 249 | } 250 | 251 | break 252 | } 253 | } 254 | } 255 | } 256 | 257 | return nil 258 | } 259 | 260 | type PagesToolBar struct { 261 | decl.ToolBar 262 | IconSize walk.Size 263 | } 264 | 265 | func (tb PagesToolBar) Create(builder *decl.Builder) error { 266 | w, err := walk.NewToolBarWithOrientationAndButtonStyle( 267 | builder.Parent(), 268 | walk.Orientation(tb.Orientation), 269 | walk.ToolBarButtonStyle(tb.ButtonStyle), 270 | ) 271 | if err != nil { 272 | return err 273 | } 274 | 275 | if tb.ToolBar.AssignTo != nil { 276 | *tb.ToolBar.AssignTo = w 277 | } 278 | if tb.IconSize.Height == 0 { 279 | tb.IconSize.Height = 16 280 | } 281 | if tb.IconSize.Width == 0 { 282 | tb.IconSize.Width = 16 283 | } 284 | 285 | return builder.InitWidget(tb, w, func() error { 286 | imageList, err := walk.NewImageList(tb.IconSize, 0) 287 | if err != nil { 288 | return err 289 | } 290 | w.SetImageList(imageList) 291 | 292 | mtr := tb.MaxTextRows 293 | if mtr < 1 { 294 | mtr = 1 295 | } 296 | if err := w.SetMaxTextRows(mtr); err != nil { 297 | return err 298 | } 299 | if err := addToActionList(w.Actions(), tb.Actions); err != nil { 300 | return err 301 | } 302 | 303 | return nil 304 | }) 305 | } 306 | 307 | func addToActionList(list *walk.ActionList, actions []*walk.Action) error { 308 | for _, a := range actions { 309 | if err := list.Add(a); err != nil { 310 | return err 311 | } 312 | } 313 | 314 | return nil 315 | } 316 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "path" 12 | "regexp" 13 | "strings" 14 | "time" 15 | 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | // checks if given node recently failed 20 | func (fm *Frontman) nodeRecentlyFailed(node *Node) bool { 21 | limit := time.Second * time.Duration(fm.Config.Node.NodeCacheErrors) 22 | 23 | fm.failedNodeLock.Lock() 24 | defer fm.failedNodeLock.Unlock() 25 | 26 | if when, ok := fm.failedNodes[node.URL]; ok { 27 | if time.Since(when) < limit { 28 | logrus.Debugf("skipping recently failed node %s", node.URL) 29 | return true 30 | } 31 | } 32 | 33 | return false 34 | } 35 | 36 | // marks a node as temporarily failing 37 | func (fm *Frontman) markNodeFailure(node *Node, data []byte) { 38 | fm.failedNodeLock.Lock() 39 | defer fm.failedNodeLock.Unlock() 40 | fm.failedNodes[node.URL] = time.Now() 41 | fm.failedNodeCache[node.URL] = data 42 | } 43 | 44 | // returns the most recent cached node failure response 45 | func (fm *Frontman) getCachedNodeFailure(node *Node) []byte { 46 | fm.failedNodeLock.Lock() 47 | defer fm.failedNodeLock.Unlock() 48 | 49 | if n, ok := fm.failedNodeCache[node.URL]; ok { 50 | return n 51 | } 52 | return nil 53 | } 54 | 55 | // asking other nodes to try a failed check 56 | func (fm *Frontman) askNodes(check Check, res *Result) { 57 | 58 | var data []byte 59 | 60 | if len(fm.Config.Nodes) < 1 { 61 | return 62 | } 63 | 64 | msg := res.Message.(string) 65 | // only forward if result message don't match ForwardExcept config 66 | if len(fm.Config.Node.ForwardExcept) > 0 { 67 | for _, rexp := range fm.Config.Node.ForwardExcept { 68 | // case insensitive match 69 | irexp := "(?i)" + rexp 70 | match, err := regexp.MatchString(irexp, msg) 71 | if err != nil { 72 | logrus.Error("forward_except regexp error ", err) 73 | } else if match { 74 | logrus.Infof("forward_except matched on '%s', won't forward %s", rexp, msg) 75 | return 76 | } 77 | } 78 | } 79 | 80 | uuid := "" 81 | checkType := "" 82 | if c, ok := check.(ServiceCheck); ok { 83 | if c.Check.Protocol == "ssl" { 84 | // ssl checks are excluded from "ask node" feature 85 | return 86 | } 87 | uuid = c.UUID 88 | checkType = "serviceCheck" 89 | req := &Input{ServiceChecks: []ServiceCheck{c}} 90 | data, _ = json.Marshal(req) 91 | } 92 | if c, ok := check.(WebCheck); ok { 93 | uuid = c.UUID 94 | checkType = "webCheck" 95 | req := &Input{WebChecks: []WebCheck{c}} 96 | data, _ = json.Marshal(req) 97 | } 98 | if c, ok := check.(SNMPCheck); ok { 99 | uuid = c.UUID 100 | checkType = "snmpCheck" 101 | req := &Input{SNMPChecks: []SNMPCheck{c}} 102 | data, _ = json.Marshal(req) 103 | } 104 | 105 | var nodeResults []string 106 | var succeededNodes []string 107 | var failedNodes []string 108 | failedNodeMessage := make(map[string]string) 109 | 110 | for i := range fm.Config.Nodes { 111 | node := fm.Config.Nodes[i] 112 | if fm.nodeRecentlyFailed(&node) { 113 | logrus.Warnf("Skipping recently failed node %s", node.URL) 114 | failedNodes = append(failedNodes, node.URL) 115 | if failure := fm.getCachedNodeFailure(&node); failure != nil { 116 | failureText, _ := json.Marshal(failure) 117 | nodeResults = append(nodeResults, string(failureText)) 118 | } 119 | continue 120 | } 121 | 122 | url, err := url.Parse(node.URL) 123 | if err != nil { 124 | logrus.Warnf("Invalid node url in config: '%s': %s", node.URL, err.Error()) 125 | continue 126 | } 127 | url.Path = path.Join(url.Path, "check") 128 | logrus.Debugf("askNodes asking %s (%s)", node.URL, check.uniqueID()) 129 | 130 | client := &http.Client{ 131 | Timeout: time.Duration(fm.Config.Node.NodeTimeout) * time.Second, 132 | } 133 | if !node.VerifySSL { 134 | client.Transport = &http.Transport{ 135 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 136 | } 137 | } 138 | 139 | fm.logForward(fmt.Sprintf("Forwarding check %s, type %s, msg '%s' to %s", uuid, checkType, msg, node.URL)) 140 | req, _ := http.NewRequest("POST", url.String(), bytes.NewBuffer(data)) 141 | req.SetBasicAuth(node.Username, node.Password) 142 | req.Header.Set("Content-Type", "application/json") 143 | resp, err := client.Do(req) 144 | if err != nil { 145 | logrus.Debugf("askNodes failed: %s (%s)", err.Error(), check.uniqueID()) 146 | fm.markNodeFailure(&node, nil) 147 | } else { 148 | defer resp.Body.Close() 149 | 150 | body, _ := ioutil.ReadAll(resp.Body) 151 | if resp.StatusCode == http.StatusOK { 152 | nodeResults = append(nodeResults, string(body)) 153 | } else { 154 | logrus.Errorf("askNodes received HTTP %v from %s", resp.StatusCode, node.URL) 155 | fm.markNodeFailure(&node, body) 156 | } 157 | } 158 | } 159 | 160 | if len(nodeResults) == 0 { 161 | // all nodes failed, use original measure 162 | logrus.Debugf("askNodes received no successful results (%s)", check.uniqueID()) 163 | return 164 | } 165 | 166 | bestDuration := 999. 167 | 168 | // select the fastest result, fall back to first result if we fail 169 | resultID := 0 170 | for currID, resp := range nodeResults { 171 | 172 | var selected []interface{} 173 | if err := json.Unmarshal([]byte(resp), &selected); err != nil { 174 | logrus.Errorf("unmarshal of node result '%v' failed: %v", resp, err) 175 | continue 176 | } 177 | 178 | // recognize response type and check relevant values 179 | if l1, ok := selected[0].(map[string]interface{}); ok { 180 | 181 | nodeName := "" 182 | if n, ok := l1["node"].(string); ok { 183 | nodeName = n 184 | } 185 | 186 | nodeMessage := "" 187 | if n, ok := l1["message"].(string); ok { 188 | nodeMessage = n 189 | } 190 | 191 | if l2, ok := l1["measurements"].(map[string]interface{}); ok { 192 | 193 | successKey := "" 194 | for key := range l2 { 195 | lastPeriod := strings.LastIndex(key, ".") 196 | if lastPeriod == -1 { 197 | continue 198 | } 199 | switch key[lastPeriod+1:] { 200 | case "success": 201 | successKey = key 202 | } 203 | } 204 | if successKey == "" { 205 | continue 206 | } 207 | 208 | if success, ok := l2[successKey].(float64); ok { 209 | if int(success) == 1 { 210 | succeededNodes = append(succeededNodes, nodeName) 211 | } else { 212 | failedNodeMessage[nodeName] = nodeMessage 213 | failedNodes = append(failedNodes, nodeName) 214 | continue 215 | } 216 | } 217 | 218 | useKey := "" 219 | for key := range l2 { 220 | lastPeriod := strings.LastIndex(key, ".") 221 | if lastPeriod == -1 { 222 | continue 223 | } 224 | switch key[lastPeriod+1:] { 225 | case "roundTripTime_s", "totalTimeSpent_s", "connectTime_s": 226 | useKey = key 227 | } 228 | } 229 | if useKey == "" { 230 | continue 231 | } 232 | if duration, ok := l2[useKey].(float64); ok { 233 | if duration < bestDuration { 234 | resultID = currID 235 | bestDuration = duration 236 | } 237 | } 238 | } 239 | } 240 | } 241 | 242 | var fastestResult []Result 243 | if err := json.Unmarshal([]byte(nodeResults[resultID]), &fastestResult); err != nil { 244 | logrus.Errorf("askNodes unmarshal of fastest node result '%v' failed: %v", nodeResults[resultID], err) 245 | } 246 | if len(fastestResult) < 1 { 247 | logrus.Warning("askNodes no results gathered from node") 248 | return 249 | } 250 | 251 | locallMeasurement := *res 252 | 253 | // make the fastest node measurement the main result 254 | *res = fastestResult[0] 255 | 256 | fastestMsg := "" 257 | if f, ok := res.Message.(string); ok { 258 | fastestMsg = f 259 | } 260 | 261 | // append all node messages to Message response 262 | nodeMsg := fm.Config.NodeName + ": " + fastestMsg + "\n" 263 | for _, v := range failedNodes { 264 | nodeMsg += fmt.Sprintf("%s: %s\n", v, failedNodeMessage[v]) 265 | } 266 | for _, v := range succeededNodes { 267 | nodeMsg += fmt.Sprintf("%s: check succeeded\n", v) 268 | } 269 | 270 | (*res).Message = nodeMsg 271 | 272 | logrus.Debug("askNodes succeess:", nodeMsg) 273 | 274 | // combine the other measurments with the failing measurement 275 | for idx := range nodeResults { 276 | if idx == resultID { 277 | continue 278 | } 279 | 280 | var result []Result 281 | if err := json.Unmarshal([]byte(nodeResults[idx]), &result); err != nil { 282 | logrus.Error(err) 283 | } 284 | 285 | var out []map[string]interface{} 286 | inrec, _ := json.Marshal(result) 287 | json.Unmarshal(inrec, &out) 288 | 289 | (*res).NodeMeasurements = append((*res).NodeMeasurements, out...) 290 | } 291 | 292 | var locallMeasurementInterface map[string]interface{} 293 | tmp, _ := json.Marshal(locallMeasurement) 294 | json.Unmarshal(tmp, &locallMeasurementInterface) 295 | 296 | (*res).NodeMeasurements = append((*res).NodeMeasurements, locallMeasurementInterface) 297 | } 298 | 299 | func (fm *Frontman) logForward(s string) { 300 | if fm.forwardLog == nil { 301 | return 302 | } 303 | t := time.Now() 304 | s = t.Format(time.RFC3339) + " " + s + "\n" 305 | fm.forwardLog.WriteString(s) 306 | } 307 | -------------------------------------------------------------------------------- /example.json: -------------------------------------------------------------------------------- 1 | { 2 | "webChecks": [{ 3 | "checkUUID": "web_head_status_matched", 4 | "check": { "url": "https://www.google.com", "method": "head", "expectedHttpStatus": 200} 5 | },{ 6 | "checkUUID": "web_get_status_matched", 7 | "check": { "url": "https://www.google.com", "method": "get", "expectedHttpStatus": 200} 8 | },{ 9 | "checkUUID": "web_get_status_not_matched", 10 | "check": { "url": "https://www.google.com/gdfgdfgdf", "method": "get", "expectedHttpStatus": 200} 11 | },{ 12 | "checkUUID": "web_follow_redirects_status_matched", 13 | "check": { "url": "https://google.com", "method": "get", "expectedHttpStatus": 200} 14 | },{ 15 | "checkUUID": "web_dont_follow_redirects_status_matched", 16 | "check": { "url": "https://google.com", "method": "get", "expectedHttpStatus": 301, "dontFollowRedirects": true} 17 | },{ 18 | "checkUUID": "web_too_many_redirections", 19 | "check": { "url": "https://httpbin.org/redirect/11", "method": "get", "expectedHttpStatus": 200} 20 | },{ 21 | "checkUUID": "web_read_timeout", 22 | "check": { "url": "https://www.google.com", "method": "get", "expectedHttpStatus": 200, "timeout": 0.01} 23 | },{ 24 | "checkUUID": "web_body_read_timeout_but_status_matched", 25 | "check": { "url": "http://ovh.net/files/1Mio.dat", "method": "get", "expectedHttpStatus": 200} 26 | },{ 27 | "checkUUID": "web_expected_text_found_1", 28 | "check": { "url": "https://en.wikipedia.org/wiki/Mars", "method": "get", "expectedHttpStatus": 200, "expectedPattern":"preserved ancient life"} 29 | },{ 30 | "checkUUID": "web_expected_html_found_2", 31 | "check": { "url": "https://en.wikipedia.org/wiki/Saturn", "method": "get", "expectedHttpStatus": 200, "searchHtmlSource": true, "expectedPattern":""} 32 | },{ 33 | "checkUUID": "web_expected_text_not_found", 34 | "check": { "url": "https://en.wikipedia.org/wiki/Neptune", "method": "get", "expectedHttpStatus": 200, "expectedPattern":"life"} 35 | },{ 36 | "checkUUID": "web_expired_ssl", 37 | "check": {"url": "https://expired.badssl.com", "method": "get", "expectedHttpStatus": 200} 38 | },{ 39 | "checkUUID": "web_basic_auth_valid", 40 | "check": { "url": "https://user:123@httpbin.org/basic-auth/user/123", "method": "get", "expectedHttpStatus": 200} 41 | },{ 42 | "checkUUID": "web_basic_auth_invalid", 43 | "check": { "url": "https://user:1234@httpbin.org/basic-auth/user/123", "method": "get", "expectedHttpStatus": 200} 44 | },{ 45 | "checkUUID": "web_post_form", 46 | "check": { "url": "https://httpbin.org/anything", "method": "post", "postData":"username=foo&password=12345", "expectedHttpStatus": 200, "searchHtmlSource": true, "expectedPattern":" \"form\": {\n \"password\": \"12345\", \n \"username\": \"foo\"\n }"} 47 | }], 48 | "serviceChecks": [{ 49 | "checkUUID": "icmp_hostname", 50 | "check": { "connect": "google.com", "protocol": "icmp", "service": "ping"} 51 | },{ 52 | "checkUUID": "icmp_ipv4", 53 | "check": { "connect": "8.8.8.8", "protocol": "icmp", "service": "ping"} 54 | },{ 55 | "checkUUID": "icmp_ipv6", 56 | "check": { "connect": "2001:4860:4860::8888", "protocol": "icmp", "service": "ping"} 57 | },{ 58 | "checkUUID": "request_timeout", 59 | "check": { "connect": "233.124.125.244", "protocol": "icmp", "service": "ping"} 60 | },{ 61 | "checkUUID": "icmp_high_latency", 62 | "check": { "connect": "www.aucklandcouncil.govt.nz", "protocol": "icmp", "service": "ping"} 63 | },{ 64 | "checkUUID": "icmp_domain_not_exists", 65 | "check": { "connect": "not_exists_domain1234.com", "protocol": "icmp", "service": "ping"} 66 | },{ 67 | "checkUUID": "tcp_ok", 68 | "check": { "connect": "google.fr", "port": 443, "protocol": "tcp", "service": "tcp"} 69 | },{ 70 | "checkUUID": "tcp_port_not_open", 71 | "check": { "connect": "google.fr", "port": 23124, "protocol": "tcp", "service": "tcp"} 72 | },{ 73 | "checkUUID": "tcp_failed", 74 | "check": { "connect": "not_exists_domain3456.com", "port": 443, "protocol": "tcp", "service": "tcp"} 75 | },{ 76 | "checkUUID": "tcp_ftp_ok", 77 | "check": { "connect": "ftp.dlptest.com", "port": 21, "protocol": "tcp", "service": "ftp"} 78 | },{ 79 | "checkUUID": "tcp_ftps_ok", 80 | "check": { "connect": "ftp.dlptest.com", "protocol": "tcp", "service": "ftps"} 81 | },{ 82 | "checkUUID": "tcp_http_ok", 83 | "check": { "connect": "google.com", "protocol": "tcp", "service": "http"} 84 | },{ 85 | "checkUUID": "tcp_https_ok", 86 | "check": { "connect": "httpbin.org", "protocol": "tcp", "service": "https"} 87 | },{ 88 | "checkUUID": "tcp_imap_ok", 89 | "check": { "connect": "imap.o2online.de", "port": 143, "protocol": "tcp", "service": "imap"} 90 | },{ 91 | "checkUUID": "tcp_imaps_ok", 92 | "check": { "connect": "imap.gmail.com", "port": 993, "protocol": "tcp", "service": "imaps"} 93 | },{ 94 | "checkUUID": "tcp_pop3_ok", 95 | "check": { "connect": "mail.btinternet.com", "port": 110, "protocol": "tcp", "service": "pop3"} 96 | },{ 97 | "checkUUID": "tcp_pop3s_ok", 98 | "check": { "connect": "pop.mail.yahoo.com", "port": 995, "protocol": "tcp", "service": "pop3s"} 99 | },{ 100 | "checkUUID": "tcp_smtp_ok", 101 | "check": { "connect": "smtp.comcast.net", "port": 587, "protocol": "tcp", "service": "smtp"} 102 | },{ 103 | "checkUUID": "tcp_smtps_ok", 104 | "check": { "connect": "smtp.gmail.com", "port": 465, "protocol": "tcp", "service": "smtps"} 105 | },{ 106 | "checkUUID": "tcp_ssh_ok", 107 | "check": { "connect": "sdf.org", "port": 22, "protocol": "tcp", "service": "ssh"} 108 | },{ 109 | "checkUUID": "tcp_ssh_invalid", 110 | "check": { "connect": "sdf.org", "protocol": "tcp", "service": "ssh", "port": 21} 111 | },{ 112 | "checkUUID": "tcp_nntp_ok", 113 | "check": { "connect": "nntp.aioe.org", "port": 119, "protocol": "tcp", "service": "nntp"} 114 | },{ 115 | "checkUUID": "tcp_ldap_ok", 116 | "check": { "connect": "ldap.forumsys.com", "port":389, "protocol": "tcp", "service": "ldap"} 117 | },{ 118 | "checkUUID": "ssl_cert_ok", 119 | "check": { "connect": "google.com", "protocol": "ssl", "service": "https"} 120 | },{ 121 | "checkUUID": "ssl_cert_by_ip", 122 | "check": { "connect": "173.194.222.100", "protocol": "ssl", "service": "https"} 123 | },{ 124 | "checkUUID": "ssl_cert_expired", 125 | "check": { "connect": "expired.badssl.com", "protocol": "ssl", "service": "https"} 126 | },{ 127 | "checkUUID": "ssl_cert_ssl_not_supported", 128 | "check": { "connect": "google.com", "protocol": "ssl", "service": "http"} 129 | },{ 130 | "checkUUID": "ssl_cert_ssl_wrong_host", 131 | "check": { "connect": "wrong.host.badssl.com", "protocol": "ssl", "service": "https"} 132 | },{ 133 | "checkUUID": "ssl_cert_ssl_self_signed", 134 | "check": { "connect": "104.154.89.105", "protocol": "ssl", "service": "https"} 135 | },{ 136 | "checkUUID": "ssl_cert_failed_to_determine_port", 137 | "check": { "connect": "google.com", "protocol": "ssl", "service": "unknown"} 138 | },{ 139 | "checkUUID": "sip_ok", 140 | "check": { "connect": "sipconnect.sipgate.de", "port": 5060, "protocol": "udp", "service": "sip"} 141 | },{ 142 | "checkUUID": "iax2_ok", 143 | "check": { "connect": "sipconnect.sipgate.de", "port": 4569, "protocol": "udp", "service": "iax2"} 144 | }], 145 | "snmpChecks": [{ 146 | "checkUUID": "snmp_basedata_v1", 147 | "check": { 148 | "connect": "172.16.72.143", 149 | "port": 161, 150 | "timeout": 1.0, 151 | "protocol": "v1", 152 | "community": "public", 153 | "preset": "basedata" 154 | }},{ 155 | "checkUUID": "snmp_basedata_v2", 156 | "check": { 157 | "connect": "172.16.72.143", 158 | "port": 161, 159 | "timeout": 1.0, 160 | "protocol": "v2", 161 | "community": "public", 162 | "preset": "basedata" 163 | }},{ 164 | "checkUUID": "snmp_basedata_v3_noAuthNoPriv", 165 | "check": { 166 | "connect": "172.16.72.143", 167 | "port": 161, 168 | "timeout": 1.0, 169 | "protocol": "v3", 170 | "preset": "basedata", 171 | "security_level": "noAuthNoPriv", 172 | "username": "noAuthNoPrivUser" 173 | }},{ 174 | "checkUUID": "snmp_basedata_v3_authNoPriv", 175 | "check": { 176 | "connect": "172.16.72.143", 177 | "port": 161, 178 | "timeout": 1.0, 179 | "protocol": "v3", 180 | "preset": "basedata", 181 | "security_level": "authNoPriv", 182 | "authentication_protocol": "sha", 183 | "username": "authOnlyUser", 184 | "authentication_password": "password" 185 | }},{ 186 | "checkUUID": "snmp_basedata_v3_authPriv", 187 | "check": { 188 | "connect": "172.16.72.143", 189 | "port": 161, 190 | "timeout": 1.0, 191 | "protocol": "v3", 192 | "preset": "basedata", 193 | "security_level": "authPriv", 194 | "authentication_protocol": "sha", 195 | "privacy_protocol": "des", 196 | "username": "authPrivUser", 197 | "authentication_password": "auth_password", 198 | "privacy_password": "priv_password" 199 | }}] 200 | } 201 | -------------------------------------------------------------------------------- /sendermode.go: -------------------------------------------------------------------------------- 1 | package frontman 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/pkg/errors" 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | func (fm *Frontman) postResultsToHub(results []Result) error { 19 | if len(results) == 0 { 20 | return nil 21 | } 22 | 23 | fm.offlineResultsLock.Lock() 24 | defer fm.offlineResultsLock.Unlock() 25 | fm.offlineResultsBuffer = append(fm.offlineResultsBuffer, results...) 26 | 27 | b, err := json.Marshal(Results{ 28 | Results: fm.offlineResultsBuffer, 29 | }) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | // in case we have HubMaxOfflineBufferBytes set(>0) and buffer + results exceed HubMaxOfflineBufferBytes -> reset buffer 35 | if fm.Config.HubMaxOfflineBufferBytes > 0 && len(b) > fm.Config.HubMaxOfflineBufferBytes && len(fm.offlineResultsBuffer) > 0 { 36 | logrus.Errorf("hub_max_offline_buffer_bytes(%d bytes) exceed with %d results. Flushing the buffer...", 37 | fm.Config.HubMaxOfflineBufferBytes, 38 | len(results)) 39 | 40 | fm.offlineResultsBuffer = results 41 | b, err = json.Marshal(Results{ 42 | Results: fm.offlineResultsBuffer, 43 | }) 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | 49 | if fm.Config.HubURL == "" { 50 | return newEmptyFieldError("hub_url") 51 | } else if u, err := url.Parse(fm.Config.HubURL); err != nil { 52 | err = errors.WithStack(err) 53 | return newFieldError("hub_url", err) 54 | } else if u.Scheme != "http" && u.Scheme != "https" { 55 | err := errors.Errorf("wrong scheme '%s', URL must start with http:// or https://", u.Scheme) 56 | return newFieldError("hub_url", err) 57 | } 58 | 59 | var req *http.Request 60 | var bodyLength int 61 | 62 | if fm.Config.HubGzip { 63 | var buffer bytes.Buffer 64 | zw := gzip.NewWriter(&buffer) 65 | if _, err := zw.Write(b); err != nil { 66 | _ = zw.Close() 67 | return err 68 | } 69 | if err := zw.Close(); err != nil { 70 | return err 71 | } 72 | 73 | req, err = http.NewRequest("POST", fm.Config.HubURL, &buffer) 74 | bodyLength = buffer.Len() 75 | req.Header.Set("Content-Encoding", "gzip") 76 | } else { 77 | req, err = http.NewRequest("POST", fm.Config.HubURL, bytes.NewBuffer(b)) 78 | bodyLength = len(b) 79 | } 80 | if err != nil { 81 | return err 82 | } 83 | 84 | req.Header.Add("User-Agent", fm.userAgent()) 85 | 86 | if fm.Config.HubUser != "" { 87 | req.SetBasicAuth(fm.Config.HubUser, fm.Config.HubPassword) 88 | } 89 | 90 | started := time.Now() 91 | 92 | resp, err := fm.hubClient.Do(req) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | defer resp.Body.Close() 98 | 99 | secondsSpent := float64(time.Since(started)) / float64(time.Second) 100 | logrus.Infof("Sent %d results to Hub.. Status %d. Spent %fs", len(fm.offlineResultsBuffer), resp.StatusCode, secondsSpent) 101 | 102 | if resp.StatusCode == 205 { 103 | logrus.Debugf("postResultsToHub hub returned 205") 104 | return ErrorHubResetContent{} 105 | } 106 | if resp.StatusCode < 200 || resp.StatusCode >= 400 { 107 | logrus.Debugf("postResultsToHub failed with %v", resp.Status) 108 | return ErrorHubGeneral{resp.StatusCode, resp.Status} 109 | } 110 | 111 | // in case of successful POST, we reset the offline buffer 112 | fm.offlineResultsBuffer = []Result{} 113 | 114 | // Update frontman statistics 115 | fm.statsLock.Lock() 116 | fm.stats.BytesSentToHubTotal += uint64(bodyLength) 117 | fm.statsLock.Unlock() 118 | 119 | return nil 120 | } 121 | 122 | func (fm *Frontman) sendResultsChanToFile(outputFile *os.File) error { 123 | var results []Result 124 | var jsonEncoder = json.NewEncoder(outputFile) 125 | for res := range fm.resultsChan { 126 | results = append(results, res) 127 | } 128 | 129 | return jsonEncoder.Encode(results) 130 | } 131 | 132 | // posts results to hub, used by RunOnce 133 | func (fm *Frontman) sendResultsChanToHub() error { 134 | var results []Result 135 | logrus.Infof("sendResultsChanToHub collecting %d results", len(fm.resultsChan)) 136 | for res := range fm.resultsChan { 137 | results = append(results, res) 138 | } 139 | 140 | logrus.Infof("sendResultsChanToHub: sending %d results", len(results)) 141 | err := fm.postResultsToHub(results) 142 | if err != nil { 143 | return fmt.Errorf("postResultsToHub: %s", err.Error()) 144 | } 145 | 146 | fm.statsLock.Lock() 147 | fm.stats.CheckResultsSentToHub += uint64(len(results)) 148 | fm.statsLock.Unlock() 149 | 150 | return nil 151 | } 152 | 153 | func (fm *Frontman) writeQueueStatsContinuous() { 154 | writeQueueStatsInterval := time.Millisecond * 200 155 | 156 | for { 157 | select { 158 | case <-fm.InterruptChan: 159 | logrus.Infof("writeQueueStatsContinuous interrupt caught, returning") 160 | return 161 | case <-time.After(writeQueueStatsInterval): 162 | fm.writeQueueStats() 163 | } 164 | } 165 | } 166 | 167 | func (fm *Frontman) pollResultsChan() { 168 | 169 | // chan polling forever until closed 170 | for res := range fm.resultsChan { 171 | fm.resultsLock.Lock() 172 | fm.results = append(fm.results, res) 173 | fm.resultsLock.Unlock() 174 | } 175 | 176 | logrus.Debugf("pollResultsChan resultsChan closed, returning") 177 | } 178 | 179 | // expire results who is past TTL 180 | func (fm *Frontman) expireOldResults() { 181 | ttlDuration := time.Duration(fm.Config.CheckResultsTTL) * time.Second 182 | 183 | fm.resultsLock.Lock() 184 | currentResults := []Result{} 185 | for _, result := range fm.results { 186 | since := time.Since(time.Unix(result.Timestamp, 0)) 187 | if since <= ttlDuration { 188 | currentResults = append(currentResults, result) 189 | } else { 190 | logrus.Debugf("result %s is %v old, expiring", result.CheckUUID, since) 191 | } 192 | } 193 | fm.results = currentResults 194 | fm.resultsLock.Unlock() 195 | } 196 | 197 | // sends results to hub continuously 198 | func (fm *Frontman) sendResultsChanToHubQueue() { 199 | 200 | sendInterval := secToDuration(float64(fm.Config.SenderInterval)) 201 | var sendResults []Result 202 | lastSentToHub := time.Unix(0, 0) 203 | 204 | for { 205 | if time.Since(lastSentToHub) >= sendInterval { 206 | lastSentToHub = time.Now() 207 | 208 | fm.expireOldResults() 209 | 210 | fm.resultsLock.Lock() 211 | if len(fm.results) >= fm.Config.SenderBatchSize { 212 | sendResults = fm.results[0:fm.Config.SenderBatchSize] 213 | fm.results = fm.results[fm.Config.SenderBatchSize:] 214 | } else { 215 | sendResults = fm.results 216 | fm.results = nil 217 | } 218 | fm.resultsLock.Unlock() 219 | 220 | currentSenders := int(atomic.LoadInt64(&fm.senderThreads)) 221 | if currentSenders < fm.Config.SenderThreadConcurrency { 222 | if len(sendResults) > 0 { 223 | logrus.Infof("sendResultsChanToHubQueue: sending %v results", len(sendResults)) 224 | fm.TerminateQueue.Add(1) 225 | 226 | atomic.AddInt64(&fm.senderThreads, 1) 227 | go func(r []Result) { 228 | defer fm.TerminateQueue.Done() 229 | defer atomic.AddInt64(&fm.senderThreads, -1) 230 | 231 | err := fm.postResultsToHub(r) 232 | 233 | if err == nil { 234 | fm.statsLock.Lock() 235 | fm.stats.CheckResultsSentToHub += uint64(len(r)) 236 | fm.statsLock.Unlock() 237 | } else { 238 | switch err.(type) { 239 | case ErrorHubResetContent: 240 | logrus.Debugf("result queue cleared") 241 | fm.resultsLock.Lock() 242 | fm.results = []Result{} 243 | fm.resultsLock.Unlock() 244 | 245 | case ErrorHubGeneral: 246 | if !fm.Config.DiscardOnHTTPResponseError { 247 | // If the hub doesn't respond with 2XX, the results remain in the queue. 248 | fm.resultsLock.Lock() 249 | fm.results = append(fm.results, r...) 250 | fm.resultsLock.Unlock() 251 | } 252 | default: 253 | if !fm.Config.DiscardOnHTTPConnectError { 254 | fm.resultsLock.Lock() 255 | fm.results = append(fm.results, r...) 256 | fm.resultsLock.Unlock() 257 | } 258 | } 259 | logrus.Errorf("postResultsToHub error: %s", err.Error()) 260 | } 261 | }(sendResults) 262 | } else { 263 | logrus.Infof("sendResultsChanToHubQueue: nothing to do. outgoing queue empty.") 264 | } 265 | } else { 266 | logrus.Errorf("Too few concurrent sender threads (%d)", currentSenders) 267 | } 268 | } 269 | 270 | select { 271 | case <-fm.InterruptChan: 272 | fm.resultsLock.RLock() 273 | logrus.Infof("sendResultsChanToHubQueue interrupt caught, posting last %d results", len(fm.results)) 274 | if err := fm.postResultsToHub(fm.results); err != nil { 275 | logrus.Error(err) 276 | } 277 | fm.resultsLock.RUnlock() 278 | return 279 | case <-time.After(250 * time.Millisecond): 280 | continue 281 | } 282 | } 283 | } 284 | 285 | func (fm *Frontman) writeQueueStats() { 286 | if fm.Config.QueueStatsFile == "" { 287 | return 288 | } 289 | 290 | fm.ipc.mutex.RLock() 291 | ipcLen := len(fm.ipc.uuids) 292 | fm.ipc.mutex.RUnlock() 293 | 294 | fm.checksLock.RLock() 295 | checksLen := len(fm.checks) 296 | fm.checksLock.RUnlock() 297 | 298 | fm.resultsLock.RLock() 299 | resultsLen := len(fm.results) 300 | fm.resultsLock.RUnlock() 301 | 302 | data, err := json.Marshal(map[string]int{ 303 | "checks_queue": checksLen, 304 | "checks_in_progress": ipcLen, 305 | "results_queue": resultsLen, 306 | "ts": int(time.Now().UnixNano())}) 307 | 308 | if err != nil { 309 | logrus.Error("writeQueueStats Marshal", err) 310 | return 311 | } 312 | 313 | go func(b []byte) { 314 | f, err := os.Create(fm.Config.QueueStatsFile) 315 | if err != nil { 316 | logrus.Error("writeQueueStats Create", err) 317 | return 318 | } 319 | defer f.Close() 320 | _, err = f.Write(b) 321 | if err != nil { 322 | logrus.Error("writeQueueStats Write", err) 323 | } 324 | }(data) 325 | } 326 | -------------------------------------------------------------------------------- /cmd/frontman/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | _ "net/http/pprof" 8 | "os" 9 | "os/signal" 10 | "runtime" 11 | "runtime/pprof" 12 | "syscall" 13 | 14 | "github.com/kardianos/service" 15 | log "github.com/sirupsen/logrus" 16 | 17 | "github.com/cloudradar-monitoring/frontman" 18 | "github.com/cloudradar-monitoring/frontman/pkg/winui" 19 | ) 20 | 21 | var exitCode = 0 22 | 23 | func exit() { 24 | os.Exit(exitCode) 25 | } 26 | 27 | func main() { 28 | // exit will be called last (FILO defer order) 29 | defer exit() 30 | 31 | systemManager := service.ChosenSystem() 32 | 33 | var serviceInstallUserPtr *string 34 | var serviceInstallPtr *bool 35 | var settingsPtr *bool 36 | var searchUpdatesPtr *bool 37 | var updatePtr *bool 38 | 39 | // Setup flag pointers 40 | inputFilePtr := flag.String("i", "", "JSON file to read the list (required)") 41 | outputFilePtr := flag.String("o", "", "file to write the results (default ./results.out)") 42 | cfgPathPtr := flag.String("c", frontman.DefaultCfgPath, "config file path") 43 | testConfigPtr := flag.Bool("t", false, "test the Hub config and exit") 44 | logLevelPtr := flag.String("v", "", "log level – overrides the level in config file (values \"error\",\"info\",\"debug\")") 45 | daemonizeModePtr := flag.Bool("d", false, "daemonize – run the process in background") 46 | oneRunOnlyModePtr := flag.Bool("r", false, "one run only – perform checks once and exit. Overwrites output file") 47 | serviceUninstallPtr := flag.Bool("u", false, fmt.Sprintf("stop and uninstall the system service(%s)", systemManager.String())) 48 | printConfigPtr := flag.Bool("p", false, "print the active config") 49 | versionPtr := flag.Bool("version", false, "show the frontman version") 50 | statsPtr := flag.Bool("stats", false, "show the frontman stats") 51 | assumeYesPtr := flag.Bool("y", false, "automatic yes to prompts. Assume 'yes' as answer to all prompts and run non-interactively") 52 | serviceStatusPtr := flag.Bool("service_status", false, "check service status") 53 | serviceStartPtr := flag.Bool("service_start", false, "start service") 54 | serviceStopPtr := flag.Bool("service_stop", false, "stop service") 55 | serviceRestartPtr := flag.Bool("service_restart", false, "restart service") 56 | serviceUpgradePtr := flag.Bool("service_upgrade", false, "upgrade service unit configuration") 57 | cpuProfile := flag.String("cpuprofile", "", "write cpu profile to file") 58 | 59 | // some OS specific flags 60 | if runtime.GOOS == "windows" { 61 | serviceInstallPtr = flag.Bool("s", false, fmt.Sprintf("install and start the system service(%s)", systemManager.String())) 62 | settingsPtr = flag.Bool("x", false, "open the settings UI") 63 | updatePtr = flag.Bool("update", false, "look for updates and apply them. Requires confirmation. Use -y to suppress the confirmation.") 64 | searchUpdatesPtr = flag.Bool("search-updates", false, "look for updates and print available") 65 | } else { 66 | serviceInstallUserPtr = flag.String("s", "", fmt.Sprintf("username to install and start the system service(%s)", systemManager.String())) 67 | } 68 | 69 | flag.Parse() 70 | // version should be handled first to ensure it will be accessible in case of fatal errors before 71 | if *versionPtr { 72 | handleFlagVersion() 73 | return 74 | } 75 | 76 | if *cpuProfile != "" { 77 | fmt.Println("Starting CPU profile") 78 | f, err := os.Create(*cpuProfile) 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | pprof.StartCPUProfile(f) 83 | defer func() { 84 | fmt.Println("Ending CPU profile") 85 | pprof.StopCPUProfile() 86 | }() 87 | } 88 | 89 | // check some incompatible flags 90 | if serviceInstallUserPtr != nil && *serviceInstallUserPtr != "" || 91 | serviceInstallPtr != nil && *serviceInstallPtr { 92 | if *inputFilePtr != "" { 93 | fmt.Println("Input file(-i) flag can't be used together with service install(-s) flag") 94 | exitCode = 1 95 | return 96 | } 97 | 98 | if *outputFilePtr != "" { 99 | fmt.Println("Output file(-o) flag can't be used together with service install(-s) flag") 100 | exitCode = 1 101 | return 102 | } 103 | 104 | if *serviceUninstallPtr { 105 | fmt.Println("Service uninstall(-u) flag can't be used together with service install(-s) flag") 106 | exitCode = 1 107 | return 108 | } 109 | 110 | if *serviceStartPtr || *serviceRestartPtr || *serviceStopPtr || *serviceStatusPtr { 111 | fmt.Println("Service management flags can't be used together with service install(-s) flag") 112 | exitCode = 1 113 | return 114 | } 115 | } 116 | 117 | cfg, err := frontman.HandleAllConfigSetup(*cfgPathPtr) 118 | if err != nil { 119 | log.Fatalf("Failed to handle frontman configuration: %s", err) 120 | } 121 | 122 | fm, err := frontman.New(cfg, *cfgPathPtr, frontman.Version) 123 | if err != nil { 124 | log.Fatalf("Failed to initialize frontman: %s", err) 125 | } 126 | 127 | if statsPtr != nil && *statsPtr { 128 | exitCode = fm.HandleFlagPrintStats() 129 | return 130 | } 131 | 132 | if printConfigPtr != nil && *printConfigPtr { 133 | fm.HandleFlagPrintConfig() 134 | return 135 | } 136 | 137 | if searchUpdatesPtr != nil && *searchUpdatesPtr { 138 | exitCode = frontman.HandleFlagSearchUpdates() 139 | return 140 | } 141 | 142 | if updatePtr != nil && *updatePtr { 143 | exitCode = frontman.HandleFlagUpdate(assumeYesPtr) 144 | return 145 | } 146 | 147 | if testConfigPtr != nil && *testConfigPtr { 148 | exitCode = fm.HandleFlagTest() 149 | return 150 | } 151 | 152 | if settingsPtr != nil && *settingsPtr { 153 | handleFlagSettings(fm) 154 | return 155 | } 156 | 157 | setDefaultLogFormatter() 158 | 159 | // log level set in flag has a precedence. If specified we need to set it ASAP 160 | if err := fm.HandleFlagLogLevel(*logLevelPtr); err != nil { 161 | log.Warn(err) 162 | } 163 | 164 | printOSSpecificWarnings() 165 | 166 | if oneRunOnlyModePtr != nil && !*oneRunOnlyModePtr { 167 | if err := fm.WritePidFileIfNeeded(); err != nil { 168 | log.Error(err) 169 | } 170 | defer fm.RemovePidFileIfNeeded() 171 | } 172 | 173 | winui.HandleFeedback(fm, *cfgPathPtr) 174 | 175 | log.Info("frontman " + frontman.Version + " started") 176 | 177 | if !*oneRunOnlyModePtr && !*testConfigPtr && *inputFilePtr == "" && *outputFilePtr == "" && cfg.HTTPListener.HTTPListen != "" { 178 | go func() { 179 | if err := fm.ServeWeb(); err != nil { 180 | log.Fatal(err) 181 | } 182 | }() 183 | } 184 | 185 | if !service.Interactive() { 186 | exitCode = fm.RunUnderOsServiceManager() 187 | return 188 | } 189 | 190 | if (serviceStatusPtr != nil && *serviceStatusPtr) || 191 | (serviceStartPtr != nil && *serviceStartPtr) || 192 | (serviceStopPtr != nil && *serviceStopPtr) || 193 | (serviceRestartPtr != nil && *serviceRestartPtr) { 194 | exitCode = fm.HandleServiceCommand(*serviceStatusPtr, *serviceStartPtr, *serviceStopPtr, *serviceRestartPtr) 195 | return 196 | } 197 | 198 | if serviceUpgradePtr != nil && *serviceUpgradePtr { 199 | exitCode = fm.HandleFlagServiceUpgrade(*cfgPathPtr, serviceUpgradePtr, serviceInstallUserPtr) 200 | return 201 | } 202 | 203 | if serviceUninstallPtr != nil && *serviceUninstallPtr { 204 | exitCode = fm.HandleFlagServiceUninstall() 205 | return 206 | } 207 | 208 | if (serviceInstallUserPtr != nil && *serviceInstallUserPtr != "") || (serviceInstallPtr != nil && *serviceInstallPtr) { 209 | exitCode = fm.HandleFlagServiceInstall(systemManager, *serviceInstallUserPtr, serviceInstallPtr, *cfgPathPtr, assumeYesPtr) 210 | return 211 | } 212 | 213 | if *daemonizeModePtr && os.Getenv("FRONTMAN_FORK") != "1" { 214 | exitCode = frontman.HandleFlagDaemonizeMode() 215 | return 216 | } 217 | 218 | if *inputFilePtr != "" && *outputFilePtr == "" { 219 | fmt.Println("Output(-o) flag can be only used together with input(-i)") 220 | exitCode = 1 221 | return 222 | } 223 | 224 | output := frontman.HandleFlagInputOutput(*inputFilePtr, *outputFilePtr, *oneRunOnlyModePtr) 225 | if output != nil { 226 | defer output.Close() 227 | } 228 | 229 | if *oneRunOnlyModePtr { 230 | exitCode = fm.HandleFlagOneRunOnlyMode(*inputFilePtr, output) 231 | return 232 | } 233 | 234 | // nothing resulted in os.Exit 235 | // so lets use the default continuous run mode and wait for interrupt 236 | signalChan := make(chan os.Signal, 1) 237 | signal.Notify(signalChan, 238 | syscall.SIGHUP, 239 | syscall.SIGINT, // ctrl-C 240 | syscall.SIGTERM) // kill 241 | 242 | go func() { 243 | fm.Run(*inputFilePtr, output) 244 | fm.DoneChan <- true 245 | }() 246 | 247 | // Handle interrupts 248 | select { 249 | case sig := <-signalChan: 250 | log.Infof("Got %s signal. Finishing the batch and exit...", sig.String()) 251 | close(fm.InterruptChan) 252 | fm.TerminateQueue.Wait() 253 | log.Infof("Stopped") 254 | return 255 | case <-fm.DoneChan: 256 | return 257 | } 258 | } 259 | 260 | func printOSSpecificWarnings() { 261 | var osNotice string 262 | if runtime.GOOS == "windows" && !frontman.CheckIfRawICMPAvailable() { 263 | osNotice = "!!! You need to run frontman as administrator in order to use ICMP ping on Windows !!!" 264 | } 265 | if runtime.GOOS == "linux" && !frontman.CheckIfRootlessICMPAvailable() && !frontman.CheckIfRawICMPAvailable() { 266 | osNotice = `⚠️ In order to perform rootless ICMP Ping on Linux you need to run this command first: 267 | sudo setcap cap_net_raw=+ep /usr/bin/frontman"` 268 | } 269 | if osNotice != "" { 270 | // print to console without log formatting 271 | fmt.Println(osNotice) 272 | 273 | // disable logging to stderr temporarily 274 | log.SetOutput(ioutil.Discard) 275 | log.Error(osNotice) 276 | log.SetOutput(os.Stderr) 277 | } 278 | } 279 | 280 | func handleFlagVersion() { 281 | fmt.Printf("frontman v%s released under MIT license. https://github.com/cloudradar-monitoring/frontman/\n", frontman.Version) 282 | } 283 | 284 | func handleFlagSettings(fm *frontman.Frontman) { 285 | winui.WindowsShowSettingsUI(fm, false) 286 | } 287 | 288 | func setDefaultLogFormatter() { 289 | textFormat := log.TextFormatter{FullTimestamp: true} 290 | if runtime.GOOS == "windows" { 291 | textFormat.DisableColors = true 292 | } 293 | log.SetFormatter(&textFormat) 294 | } 295 | --------------------------------------------------------------------------------