Files
cloud-ip-validator/internal/analytics/lists.go
T

172 lines
5.9 KiB
Go
Raw Normal View History

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)
}
}
2026-10-04 10:26:37 +03:00
sort.Slice(out, func(i, j int) bool { return lessIP(out[i].res.IPAddress, out[j].res.IPAddress) })
return out
}
2026-10-04 10:26:37 +03:00
// lessIP orders addresses numerically; text that is not an address by its text.
func lessIP(a, b string) bool {
x, errX := netip.ParseAddr(a)
y, errY := netip.ParseAddr(b)
if errX != nil || errY != nil {
return a < b
}
return x.Less(y)
}
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
}