mirror of https://github.com/statping/statping
Add SSH checker
parent
b6ca1a5c66
commit
fdeb7fe31a
|
@ -19,6 +19,7 @@
|
||||||
<option value="udp">UDP {{ $t('service') }}</option>
|
<option value="udp">UDP {{ $t('service') }}</option>
|
||||||
<option value="icmp">ICMP Ping</option>
|
<option value="icmp">ICMP Ping</option>
|
||||||
<option value="grpc">gRPC {{ $t('service') }}</option>
|
<option value="grpc">gRPC {{ $t('service') }}</option>
|
||||||
|
<option value="ssh">SSH {{ $t('service') }}</option>
|
||||||
<option value="static">Static {{ $t('service') }}</option>
|
<option value="static">Static {{ $t('service') }}</option>
|
||||||
</select>
|
</select>
|
||||||
<small class="form-text text-muted">Use HTTP if you are checking a website or use TCP if you are checking a server</small>
|
<small class="form-text text-muted">Use HTTP if you are checking a website or use TCP if you are checking a server</small>
|
||||||
|
@ -82,7 +83,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="service.type.match(/^(tcp|udp|grpc)$/)" class="form-group row">
|
<div v-if="service.type.match(/^(tcp|udp|grpc|ssh)$/)" class="form-group row">
|
||||||
<label class="col-sm-4 col-form-label">Port</label>
|
<label class="col-sm-4 col-form-label">Port</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<input v-model.number="service.port" type="number" name="port" class="form-control" id="service_port" placeholder="8080">
|
<input v-model.number="service.port" type="number" name="port" class="form-control" id="service_port" placeholder="8080">
|
||||||
|
@ -193,6 +194,55 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="service.type.match(/^(ssh)$/)" class="form-group row">
|
||||||
|
<label for="service_username" class="col-sm-4 col-form-label">{{ $t('username') }}</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input v-model="service.username" type="text" name="service_username" class="form-control" id="service_username">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="service.type.match(/^(ssh)$/)" class="form-group row">
|
||||||
|
<label for="service_password" class="col-sm-4 col-form-label">{{ $t('password') }}</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input v-model="service.password" type="text" name="service_password" class="form-control" id="service_password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="service.type.match(/^(ssh)$/)" class="form-group row">
|
||||||
|
<label class="col-12 col-md-4 col-form-label">SSH Health Check</label>
|
||||||
|
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
|
||||||
|
<span @click="service.ssh_health_check = !!service.ssh_health_check" class="switch float-left">
|
||||||
|
<input v-model="service.ssh_health_check" type="checkbox" name="ssh_health_check-option" class="switch" id="switch-ssh-health-check" v-bind:checked="service.ssh_health_check">
|
||||||
|
<label for="switch-ssh-health-check" v-if="service.ssh_health_check">Check against SSH health check command.</label>
|
||||||
|
<label for="switch-ssh-health-check" v-if="!service.ssh_health_check">Only checks if SSH connection can be established.</label>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="service.ssh_health_check" class="form-group row">
|
||||||
|
<label class="col-sm-4 col-form-label">Expected Response</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<textarea v-model="service.expected" class="form-control" rows="3" autocapitalize="none" spellcheck="false"></textarea>
|
||||||
|
<small class="form-text text-muted">Only stdout will be parsed</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="service.ssh_health_check" class="form-group row">
|
||||||
|
<label for="service_response_code" class="col-sm-4 col-form-label">Expected Exit Code</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input v-model="service.expected_status" type="number" name="expected_status" class="form-control" placeholder="1" id="service_response_code">
|
||||||
|
<small class="form-text text-muted">By convention a command exit code 0 indicates success</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="service.type.match(/^(ssh)$/) && service.ssh_health_check" class="form-group row">
|
||||||
|
<label for="check_command" class="col-sm-4 col-form-label">Check Command</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input v-model="service.check_command" type="text" name="check_command" class="form-control" id="check_command">
|
||||||
|
<small class="form-text text-muted">Check command to be executed on remote server</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="service.type.match(/^(tcp|http)$/)" class="form-group row">
|
<div v-if="service.type.match(/^(tcp|http)$/)" class="form-group row">
|
||||||
<label class="col-12 col-md-4 col-form-label">{{ $t('tls_cert') }}</label>
|
<label class="col-12 col-md-4 col-form-label">{{ $t('tls_cert') }}</label>
|
||||||
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
|
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
|
||||||
|
@ -303,6 +353,10 @@
|
||||||
order: 1,
|
order: 1,
|
||||||
verify_ssl: true,
|
verify_ssl: true,
|
||||||
grpc_health_check: false,
|
grpc_health_check: false,
|
||||||
|
ssh_health_check: false,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
check_command: "",
|
||||||
redirect: true,
|
redirect: true,
|
||||||
allow_notifications: true,
|
allow_notifications: true,
|
||||||
notify_all_changes: true,
|
notify_all_changes: true,
|
||||||
|
@ -351,6 +405,9 @@
|
||||||
this.service.port = 50051
|
this.service.port = 50051
|
||||||
this.service.verify_ssl = false
|
this.service.verify_ssl = false
|
||||||
this.service.method = ""
|
this.service.method = ""
|
||||||
|
} else if (this.service.type == "ssh") {
|
||||||
|
this.service.port = 22;
|
||||||
|
this.service.expected_status = 0;
|
||||||
} else {
|
} else {
|
||||||
this.service.expected_status = 200
|
this.service.expected_status = 200
|
||||||
this.service.expected = ""
|
this.service.expected = ""
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/statping/statping/types/metrics"
|
"github.com/statping/statping/types/metrics"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ CheckLoop:
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseHost(s *Service) string {
|
func parseHost(s *Service) string {
|
||||||
if s.Type == "tcp" || s.Type == "udp" || s.Type == "grpc" {
|
if s.Type == "tcp" || s.Type == "udp" || s.Type == "grpc" || s.Type == "ssh" {
|
||||||
return s.Domain
|
return s.Domain
|
||||||
} else {
|
} else {
|
||||||
u, err := url.Parse(s.Domain)
|
u, err := url.Parse(s.Domain)
|
||||||
|
@ -74,7 +75,7 @@ func dnsCheck(s *Service) (int64, error) {
|
||||||
var err error
|
var err error
|
||||||
t1 := utils.Now()
|
t1 := utils.Now()
|
||||||
host := parseHost(s)
|
host := parseHost(s)
|
||||||
if s.Type == "tcp" || s.Type == "udp" || s.Type == "grpc" {
|
if s.Type == "tcp" || s.Type == "udp" || s.Type == "grpc" || s.Type == "ssh" {
|
||||||
_, err = net.LookupHost(host)
|
_, err = net.LookupHost(host)
|
||||||
} else {
|
} else {
|
||||||
_, err = net.LookupIP(host)
|
_, err = net.LookupIP(host)
|
||||||
|
@ -393,6 +394,97 @@ func CheckHttp(s *Service, record bool) (*Service, error) {
|
||||||
return s, err
|
return s, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckSsh will check a SSH service
|
||||||
|
func CheckSsh(s *Service, record bool) (*Service, error) {
|
||||||
|
defer s.updateLastCheck()
|
||||||
|
timer := prometheus.NewTimer(metrics.ServiceTimer(s.Name))
|
||||||
|
defer timer.ObserveDuration()
|
||||||
|
|
||||||
|
dnsLookup, err := dnsCheck(s)
|
||||||
|
if err != nil {
|
||||||
|
if record {
|
||||||
|
RecordFailure(s, fmt.Sprintf("Could not get IP address for SSH service %v, %v", s.Domain, err), "lookup")
|
||||||
|
}
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
s.PingTime = dnsLookup
|
||||||
|
t1 := utils.Now()
|
||||||
|
domain := fmt.Sprintf("%v", s.Domain)
|
||||||
|
if s.Port != 0 {
|
||||||
|
domain = fmt.Sprintf("%v:%v", s.Domain, s.Port)
|
||||||
|
if isIPv6(s.Domain) {
|
||||||
|
domain = fmt.Sprintf("[%v]:%v", s.Domain, s.Port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sshConfig := &ssh.ClientConfig{
|
||||||
|
User: s.Username,
|
||||||
|
Auth: []ssh.AuthMethod{ssh.Password(s.Password)},
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
Timeout: time.Duration(s.Timeout) * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := ssh.Dial("tcp", domain, sshConfig)
|
||||||
|
if err != nil {
|
||||||
|
if record {
|
||||||
|
RecordFailure(s, fmt.Sprintf("Dial Error: %v", err), "ssh")
|
||||||
|
}
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
session, err := client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
if record {
|
||||||
|
RecordFailure(s, fmt.Sprintf("Failed to create SSH session: %v", err), "ssh")
|
||||||
|
}
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.SshHealthCheck.Bool {
|
||||||
|
output, err := session.Output(s.CheckCommand)
|
||||||
|
if err != nil {
|
||||||
|
if exitError, ok := err.(*ssh.ExitError); ok {
|
||||||
|
s.LastStatusCode = exitError.Waitmsg.ExitStatus()
|
||||||
|
} else {
|
||||||
|
if record {
|
||||||
|
RecordFailure(s, fmt.Sprintf("Failed to execute check command: %v", err), "ssh")
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.LastResponse = strings.TrimSpace(string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Latency = utils.Now().Sub(t1).Microseconds()
|
||||||
|
s.Online = true
|
||||||
|
|
||||||
|
if s.SshHealthCheck.Bool {
|
||||||
|
if s.ExpectedStatus != s.LastStatusCode {
|
||||||
|
if record {
|
||||||
|
RecordFailure(s, fmt.Sprintf("SSH Service: '%s', Exit Code: expected '%v', got '%v'", s.Name, s.ExpectedStatus, s.LastStatusCode), "response_code")
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Expected.String != s.LastResponse {
|
||||||
|
log.Warnln(fmt.Sprintf("SSH Service: '%s', Command output: expected '%v', got '%v'", s.Name, s.Expected.String, s.LastResponse))
|
||||||
|
if record {
|
||||||
|
RecordFailure(s, fmt.Sprintf("SSH Command output '%v' did not match '%v'", s.LastResponse, s.Expected.String), "response_body")
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if record {
|
||||||
|
RecordSuccess(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
// RecordSuccess will create a new 'hit' record in the database for a successful/online service
|
// RecordSuccess will create a new 'hit' record in the database for a successful/online service
|
||||||
func RecordSuccess(s *Service) {
|
func RecordSuccess(s *Service) {
|
||||||
s.LastOnline = utils.Now()
|
s.LastOnline = utils.Now()
|
||||||
|
@ -460,5 +552,7 @@ func (s *Service) CheckService(record bool) {
|
||||||
CheckGrpc(s, record)
|
CheckGrpc(s, record)
|
||||||
case "icmp":
|
case "icmp":
|
||||||
CheckIcmp(s, record)
|
CheckIcmp(s, record)
|
||||||
|
case "ssh":
|
||||||
|
CheckSsh(s, record)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,10 @@ type Service struct {
|
||||||
Order int `gorm:"default:0;column:order_id" json:"order_id" yaml:"order_id"`
|
Order int `gorm:"default:0;column:order_id" json:"order_id" yaml:"order_id"`
|
||||||
VerifySSL null.NullBool `gorm:"default:false;column:verify_ssl" json:"verify_ssl" scope:"user,admin" yaml:"verify_ssl"`
|
VerifySSL null.NullBool `gorm:"default:false;column:verify_ssl" json:"verify_ssl" scope:"user,admin" yaml:"verify_ssl"`
|
||||||
GrpcHealthCheck null.NullBool `gorm:"default:false;column:grpc_health_check" json:"grpc_health_check" scope:"user,admin" yaml:"grpc_health_check"`
|
GrpcHealthCheck null.NullBool `gorm:"default:false;column:grpc_health_check" json:"grpc_health_check" scope:"user,admin" yaml:"grpc_health_check"`
|
||||||
|
Username string `gorm:"column:username" json:"username" yaml:"username" scope:"user,admin"`
|
||||||
|
Password string `gorm:"column:password" json:"password" yaml:"password" scope:"user,admin"`
|
||||||
|
CheckCommand string `gorm:"column:check_command" json:"check_command" yaml:"check_command" scope:"user,admin"`
|
||||||
|
SshHealthCheck null.NullBool `gorm:"default:false;column:ssh_health_check" json:"ssh_health_check" scope:"user,admin" yaml:"ssh_health_check"`
|
||||||
Public null.NullBool `gorm:"default:true;column:public" json:"public" yaml:"public"`
|
Public null.NullBool `gorm:"default:true;column:public" json:"public" yaml:"public"`
|
||||||
GroupId int `gorm:"default:0;column:group_id" json:"group_id" yaml:"group_id"`
|
GroupId int `gorm:"default:0;column:group_id" json:"group_id" yaml:"group_id"`
|
||||||
TLSCert null.NullString `gorm:"column:tls_cert" json:"tls_cert" scope:"user,admin" yaml:"tls_cert"`
|
TLSCert null.NullString `gorm:"column:tls_cert" json:"tls_cert" scope:"user,admin" yaml:"tls_cert"`
|
||||||
|
|
Loading…
Reference in New Issue