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) } }