Files
go-sip/deploys/preprod/probe.go
T

283 lines
7.5 KiB
Go

package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"flag"
"fmt"
"net"
"net/http"
"os"
"syscall"
"time"
"git.ipao.vip/rogee/go-sip/internal/config"
"git.ipao.vip/rogee/go-sip/internal/configread"
"git.ipao.vip/rogee/go-sip/internal/rpc"
amqp "github.com/rabbitmq/amqp091-go"
"github.com/santhosh-tekuri/jsonschema/v6"
)
type input struct {
SaaSURL string `json:"saas_url"`
DispatcherID string `json:"dispatcher_id"`
Secret string `json:"secret"`
MQHost string `json:"mq_host"`
MQPort int `json:"mq_port"`
MQUser string `json:"mq_user"`
MQPassword string `json:"mq_password"`
MQVhost string `json:"mq_vhost"`
ConnectionName string `json:"connection_name"`
HoldMQMS int `json:"hold_mq_ms"`
}
type report struct {
Success bool `json:"success"`
Phase string `json:"phase"`
HTTPStatus int `json:"http_status"`
ErrorClass string `json:"error_class,omitempty"`
TransportFailure string `json:"transport_failure,omitempty"`
SchemaLocations []string `json:"schema_locations,omitempty"`
Trunks int `json:"trunks"`
Providers int `json:"providers"`
Tasks int `json:"tasks"`
ValidatedTasks int `json:"validated_tasks"`
MQConnected bool `json:"mq_connected"`
MQChannelClosed bool `json:"mq_channel_closed"`
MQConnectionClosed bool `json:"mq_connection_closed"`
}
type transport struct{ report *report }
func (t transport) RoundTrip(req *http.Request) (*http.Response, error) {
response, err := http.DefaultTransport.RoundTrip(req)
if response != nil {
t.report.HTTPStatus = response.StatusCode
}
if err != nil {
t.report.TransportFailure = transportFailure(err)
}
return response, err
}
func transportFailure(err error) string {
var verification *tls.CertificateVerificationError
var authority x509.UnknownAuthorityError
var hostname x509.HostnameError
var invalid x509.CertificateInvalidError
var roots x509.SystemRootsError
if errors.As(err, &verification) || errors.As(err, &authority) || errors.As(err, &hostname) || errors.As(err, &invalid) || errors.As(err, &roots) {
return "tls_failed"
}
var network net.Error
if errors.Is(err, syscall.ECONNREFUSED) || (errors.As(err, &network) && network.Timeout()) {
return "unavailable"
}
return "transport_failed"
}
func rejected(r *report, err error) {
r.Success = false
r.ErrorClass = fmt.Sprintf("%T", err)
var validation *jsonschema.ValidationError
if errors.As(err, &validation) {
var visit func(*jsonschema.ValidationError)
visit = func(e *jsonschema.ValidationError) {
if len(e.Causes) == 0 {
r.SchemaLocations = append(r.SchemaLocations, fmt.Sprint(e.InstanceLocation))
}
for _, cause := range e.Causes {
visit(cause)
}
}
visit(validation)
}
}
func check(in input) (r report) {
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
r.Phase = "mq_connect"
uri := amqp.URI{Scheme: "amqp", Host: in.MQHost, Port: in.MQPort, Username: in.MQUser, Password: in.MQPassword, Vhost: in.MQVhost}
conn, err := amqp.DialConfig(uri.String(), amqp.Config{Heartbeat: 5 * time.Second, Locale: "en_US", Properties: amqp.Table{"connection_name": in.ConnectionName}, Dial: func(network, addr string) (net.Conn, error) {
c, err := (&net.Dialer{Timeout: 8 * time.Second}).DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
if err := c.SetDeadline(time.Now().Add(15 * time.Second)); err != nil {
return nil, errors.Join(err, c.Close())
}
return c, nil
}})
if err != nil {
rejected(&r, err)
return
}
r.MQConnected = true
if in.HoldMQMS > 0 && in.HoldMQMS <= 10000 {
time.Sleep(time.Duration(in.HoldMQMS) * time.Millisecond)
}
r.Phase = "mq_channel"
channel, err := conn.Channel()
if err != nil {
rejected(&r, errors.Join(err, conn.Close()))
return
}
if err := channel.Close(); err != nil {
rejected(&r, errors.Join(err, conn.Close()))
return
}
r.MQChannelClosed = true
if err := conn.Close(); err != nil {
rejected(&r, err)
return
}
r.MQConnectionClosed = true
r.Phase = "saas_client"
client, err := configread.NewClient(in.SaaSURL, in.DispatcherID, in.Secret, &http.Client{Timeout: 15 * time.Second, Transport: transport{&r}})
if err != nil {
rejected(&r, err)
return
}
r.Phase = "sip"
sip, err := client.ReadSIP(ctx)
if err != nil {
rejected(&r, err)
return
}
var trunks []json.RawMessage
if err := json.Unmarshal(sip.Trunks, &trunks); err != nil {
rejected(&r, err)
return
}
r.Trunks = len(trunks)
r.Phase = "providers"
providers, err := client.ReadProviders(ctx)
if err != nil {
rejected(&r, err)
return
}
r.Providers = len(providers)
var tasks []configread.DiscoveredTask
cursor := ""
seen := map[string]bool{}
for {
r.Phase = "tasks"
page, err := client.ReadTasks(ctx, cursor)
if err != nil {
rejected(&r, err)
return
}
for _, task := range page.Tasks {
if seen[task.TaskID] {
rejected(&r, errors.New("duplicate discovery task"))
return
}
seen[task.TaskID] = true
tasks = append(tasks, task)
}
if page.Cursor == "" {
break
}
cursor = page.Cursor
}
r.Tasks = len(tasks)
for _, task := range tasks {
r.Phase = "task_and_quota"
if _, err := client.ReadTask(ctx, task.TaskID, task.TenantID, sip, providers); err != nil {
rejected(&r, err)
return
}
r.ValidatedTasks++
}
r.Phase = "complete"
r.Success = true
return
}
// environmentCheck opens only explicit local configuration and certificate
// files. It never opens SQLite, network connections, or cloud requests.
func environmentCheck(role string) (r report) {
var ca, cert, key string
r.Phase = role + "_environment"
if role == "agent" {
s, err := config.LoadAgentEnvironment("nonprod-real")
if err != nil {
rejected(&r, err)
return
}
ca, cert, key = s.CAFile, s.CertFile, s.KeyFile
} else if role == "dispatcher" {
s, err := config.LoadDispatcherRuntimeEnvironment("nonprod-real")
if err != nil {
rejected(&r, err)
return
}
r.Phase = "agent_inventory"
if _, err := config.LoadMockAgentEndpoint(s.AgentEndpointsFile); err != nil {
rejected(&r, err)
return
}
r.Phase = "oss_configuration"
if _, err := config.LoadNonprodRealOSSConfig(s.OSSConfigFile, s.DispatcherID); err != nil {
rejected(&r, err)
return
}
ca, cert, key = s.CAFile, s.CertFile, s.KeyFile
} else {
rejected(&r, errors.New("unknown environment role"))
return
}
r.Phase = role + "_tls"
caBytes, err := os.ReadFile(ca)
if err != nil {
rejected(&r, err)
return
}
certBytes, err := os.ReadFile(cert)
if err != nil {
rejected(&r, err)
return
}
keyBytes, err := os.ReadFile(key)
if err != nil {
rejected(&r, err)
return
}
if _, err := rpc.NewServerTLSConfig(caBytes, certBytes, keyBytes); err != nil {
rejected(&r, err)
return
}
r.Phase = "environment_complete"
r.Success = true
return
}
func main() {
role := flag.String("environment", "", "pure agent or dispatcher deployment configuration inspection")
flag.Parse()
if *role != "" {
r := environmentCheck(*role)
if err := json.NewEncoder(os.Stdout).Encode(r); err != nil {
os.Exit(2)
}
if !r.Success {
os.Exit(1)
}
return
}
var in input
d := json.NewDecoder(os.Stdin)
d.DisallowUnknownFields()
if err := d.Decode(&in); err != nil {
_ = json.NewEncoder(os.Stdout).Encode(report{Phase: "input", ErrorClass: fmt.Sprintf("%T", err)})
os.Exit(1)
}
r := check(in)
if err := json.NewEncoder(os.Stdout).Encode(r); err != nil {
os.Exit(2)
}
if !r.Success {
os.Exit(1)
}
}