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