package analytics import ( "fmt" "net/netip" "sort" "strings" "cloudipvalidator/internal/db" ) // List kinds served by Analysis.List. const ( ListEgressHTTPSAny = "egress_https_any" ListEgressHTTPSAll = "egress_https_all" ListIngressSSHAny = "ingress_ssh_any" ListIngressSSHAll = "ingress_ssh_all" ListError = "error" // Addresses by the verdict the system gave them at aggregation. ListVerdictPass = "verdict_" + db.ResultPass ListVerdictPartial = "verdict_" + db.ResultPartial ListVerdictFail = "verdict_" + db.ResultFail ) // List is a table of addresses behind one indicator or one error class. type List struct { Kind string `json:"kind"` Class string `json:"class,omitempty"` Columns []string `json:"columns"` Rows [][]string `json:"rows"` } // ErrUnknownList is returned for a list kind that does not exist. type ErrUnknownList string func (e ErrUnknownList) Error() string { return fmt.Sprintf("unknown list %q", string(e)) } // List builds the table for a kind; class is only used with ListError. func (an *Analysis) List(kind, class string) (*List, error) { l := &List{Kind: kind, Class: class, Rows: [][]string{}} switch kind { case ListEgressHTTPSAny, ListEgressHTTPSAll: l.Columns = []string{"Адрес", "Подсеть", "Валидатор", "https-проверок", "Проваленные цели"} if kind == ListEgressHTTPSAny { l.Columns[3] = "Провалено https" } for _, a := range an.sorted() { if a.https.n == 0 || a.https.ok == a.https.n || (kind == ListEgressHTTPSAll && a.https.ok != 0) { continue } targets := append([]string(nil), a.https.failedTargets...) sort.Strings(targets) count := fmt.Sprint(a.https.n) if kind == ListEgressHTTPSAny { count = fmt.Sprintf("%d из %d", a.https.n-a.https.ok, a.https.n) } l.Rows = append(l.Rows, []string{a.res.IPAddress, a.subnet, ShortValidator(a.https.validator), count, strings.Join(targets, ", ")}) } case ListIngressSSHAny, ListIngressSSHAll: l.Columns = []string{"Адрес", "Подсеть", "Провалено ssh", "Площадки с провалом", "Ошибка"} if kind == ListIngressSSHAll { l.Columns = []string{"Адрес", "Подсеть", "Площадки с провалом ssh", "Ошибка"} } for _, a := range an.sorted() { if a.ssh.n == 0 || a.ssh.ok == a.ssh.n || (kind == ListIngressSSHAll && a.ssh.ok != 0) { continue } sites := an.sortSites(a.ssh.sites) errs := make([]string, 0, len(a.ssh.errs)) for e := range a.ssh.errs { errs = append(errs, e) } sort.Strings(errs) if kind == ListIngressSSHAny { l.Rows = append(l.Rows, []string{a.res.IPAddress, a.subnet, fmt.Sprintf("%d из %d", len(a.ssh.sites), a.ssh.n), strings.Join(sites, ", "), strings.Join(errs, ", ")}) } else { l.Rows = append(l.Rows, []string{a.res.IPAddress, a.subnet, strings.Join(sites, ", "), strings.Join(errs, ", ")}) } } case ListVerdictPass, ListVerdictPartial, ListVerdictFail: verdict := strings.TrimPrefix(kind, "verdict_") l.Columns = []string{"Адрес", "Подсеть", "Валидатор", "Egress", "Ingress", "Проверок в цикле"} if kind == ListVerdictPartial { l.Columns = append(l.Columns, "Причина") } for _, a := range an.sorted() { if a.res.Verdict != verdict { continue } row := []string{a.res.IPAddress, a.subnet, orDash(ShortValidator(a.https.validator)), okOf(a.egress), okOf(a.ingress), a.checksCell()} if kind == ListVerdictPartial { row = append(row, reasonName(a.egress.ok < a.egress.n, a.ingress.ok < a.ingress.n, a.incomplete())) } l.Rows = append(l.Rows, row) } case ListError: if class == "" { return nil, ErrUnknownList("error without class") } l.Columns = []string{"Адрес", "Подсеть", "Площадка", "Валидатор", "Вердикт адреса", "Статус проверки"} for _, a := range an.sorted() { fs := append([]failedIngress(nil), a.failedIngress...) sort.SliceStable(fs, func(i, j int) bool { return an.siteIndex(fs[i].site) < an.siteIndex(fs[j].site) }) for _, f := range fs { if f.class != class { continue } status := "провал, в вердикте" if f.late { status = "провал, после вердикта" } l.Rows = append(l.Rows, []string{a.res.IPAddress, a.subnet, f.site, ShortValidator(f.validator), a.res.Verdict, status}) } } default: return nil, ErrUnknownList(kind) } return l, nil } // checksCell is "stored из expected", or just stored when expected is unknown. func (a *addr) checksCell() string { if a.res.ExpectedChecks < 0 { return fmt.Sprint(a.stored) } return fmt.Sprintf("%d из %d", a.stored, a.res.ExpectedChecks) } // okOf is "successful из all" of one level, "—" when it has no checks. func okOf(t typeStat) string { if t.n == 0 { return "—" } return fmt.Sprintf("%d из %d", t.ok, t.n) } func orDash(s string) string { if s == "" { return "—" } return s } // sorted returns the non-cancelled addresses in numeric address order. func (an *Analysis) sorted() []*addr { out := make([]*addr, 0, len(an.addrs)) for _, a := range an.addrs { if a.res.Verdict != db.ResultCancelled { out = append(out, a) } } sort.Slice(out, func(i, j int) bool { x, errX := netip.ParseAddr(out[i].res.IPAddress) y, errY := netip.ParseAddr(out[j].res.IPAddress) if errX != nil || errY != nil { return out[i].res.IPAddress < out[j].res.IPAddress } return x.Less(y) }) return out } func (an *Analysis) siteIndex(site string) int { return siteIndexOf(an.siteNames, site) } func (an *Analysis) sortSites(sites []string) []string { out := append([]string(nil), sites...) sort.SliceStable(out, func(i, j int) bool { return an.siteIndex(out[i]) < an.siteIndex(out[j]) }) return out }