283 lines
7.5 KiB
Go
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)
|
|
}
|
|
}
|