console: use common/schema for dimensions

This is a bit less type-safe. We could keep type safety by redefining
all the consts in `query_consts.go` in `common/schema`, but this is
pointless as the goal is to have arbitrary dimensions at some point.
This commit is contained in:
Vincent Bernat
2023-01-03 18:40:19 +01:00
parent 1f94ca76f1
commit a30024cfa1
18 changed files with 180 additions and 227 deletions

1
.gitignore vendored
View File

@@ -5,7 +5,6 @@
*.pb.go *.pb.go
mock_*.go mock_*.go
/console/frontend/data/fields.json
/console/data/frontend/ /console/data/frontend/
*~ *~

View File

@@ -16,8 +16,7 @@ export CGO_ENABLED=0
FLOW_VERSION := $(shell sed -n 's/^const CurrentSchemaVersion = //p' inlet/flow/schemas.go) FLOW_VERSION := $(shell sed -n 's/^const CurrentSchemaVersion = //p' inlet/flow/schemas.go)
GENERATED_JS = \ GENERATED_JS = \
console/frontend/node_modules \ console/frontend/node_modules
console/frontend/data/fields.json
GENERATED_GO = \ GENERATED_GO = \
inlet/flow/decoder/flow-ANY.pb.go \ inlet/flow/decoder/flow-ANY.pb.go \
common/clickhousedb/mocks/mock_driver.go \ common/clickhousedb/mocks/mock_driver.go \
@@ -99,10 +98,6 @@ console/filter/parser.go: console/filter/parser.peg | $(PIGEON) ; $(info $(M) ge
console/frontend/node_modules: console/frontend/package.json console/frontend/package-lock.json console/frontend/node_modules: console/frontend/package.json console/frontend/package-lock.json
console/frontend/node_modules: ; $(info $(M) fetching node modules) console/frontend/node_modules: ; $(info $(M) fetching node modules)
$Q (cd console/frontend ; npm ci --silent --no-audit --no-fund) && touch $@ $Q (cd console/frontend ; npm ci --silent --no-audit --no-fund) && touch $@
console/frontend/data/fields.json: console/query_consts.go ; $(info $(M) generate list of selectable fields)
$Q sed -En -e 's/^\tqueryColumn([a-zA-Z0-9]+)( .*|$$)/ "\1"/p' $< \
| sed -E -e '$$ ! s/$$/,/' -e '1s/^ */[/' -e '$$s/$$/]/' > $@
$Q test -s $@
console/data/frontend: $(GENERATED_JS) console/data/frontend: $(GENERATED_JS)
console/data/frontend: $(shell $(LSFILES) console/frontend 2> /dev/null) console/data/frontend: $(shell $(LSFILES) console/frontend 2> /dev/null)
console/data/frontend: ; $(info $(M) building console frontend) console/data/frontend: ; $(info $(M) building console frontend)

View File

@@ -36,6 +36,8 @@ const (
SkipAliasedColumns SkipAliasedColumns
// SkipTimeReceived skips the time received column // SkipTimeReceived skips the time received column
SkipTimeReceived SkipTimeReceived
// SkipNotDimension skips columns that cannot be used as a dimension
SkipNotDimension
// UseTransformFromType uses the type from TransformFrom if any // UseTransformFromType uses the type from TransformFrom if any
UseTransformFromType UseTransformFromType
// SubstituteGenerates changes the column name to use the default generated value // SubstituteGenerates changes the column name to use the default generated value
@@ -80,6 +82,9 @@ func (schema Schema) iterate(fn func(column Column), options ...TableOption) {
if slices.Contains(options, SkipAliasedColumns) && column.Alias != "" { if slices.Contains(options, SkipAliasedColumns) && column.Alias != "" {
continue continue
} }
if slices.Contains(options, SkipNotDimension) && column.NotSelectable {
continue
}
if slices.Contains(options, UseTransformFromType) && column.TransformFrom != nil { if slices.Contains(options, UseTransformFromType) && column.TransformFrom != nil {
for _, ocol := range column.TransformFrom { for _, ocol := range column.TransformFrom {
// We assume we only need to use name/type // We assume we only need to use name/type

View File

@@ -28,11 +28,12 @@ var Flows = Schema{
}, },
Columns: buildMapFromColumns([]Column{ Columns: buildMapFromColumns([]Column{
{ {
Name: "TimeReceived", Name: "TimeReceived",
Type: "DateTime", Type: "DateTime",
Codec: "DoubleDelta, LZ4", Codec: "DoubleDelta, LZ4",
NotSelectable: true,
}, },
{Name: "SamplingRate", Type: "UInt64"}, {Name: "SamplingRate", Type: "UInt64", NotSelectable: true},
{Name: "ExporterAddress", Type: "LowCardinality(IPv6)"}, {Name: "ExporterAddress", Type: "LowCardinality(IPv6)"},
{Name: "ExporterName", Type: "LowCardinality(String)", NotSortingKey: true}, {Name: "ExporterName", Type: "LowCardinality(String)", NotSortingKey: true},
{Name: "ExporterGroup", Type: "LowCardinality(String)", NotSortingKey: true}, {Name: "ExporterGroup", Type: "LowCardinality(String)", NotSortingKey: true},
@@ -45,9 +46,10 @@ var Flows = Schema{
Type: "IPv6", Type: "IPv6",
MainOnly: true, MainOnly: true,
}, { }, {
Name: "SrcNetMask", Name: "SrcNetMask",
Type: "UInt8", Type: "UInt8",
MainOnly: true, MainOnly: true,
NotSelectable: true,
}, { }, {
Name: "SrcNetPrefix", Name: "SrcNetPrefix",
Type: "String", Type: "String",
@@ -110,7 +112,8 @@ END`,
{Name: "DstLargeCommunities.LocalData1", Type: "Array(UInt32)"}, {Name: "DstLargeCommunities.LocalData1", Type: "Array(UInt32)"},
{Name: "DstLargeCommunities.LocalData2", Type: "Array(UInt32)"}, {Name: "DstLargeCommunities.LocalData2", Type: "Array(UInt32)"},
}, },
TransformTo: "arrayMap((asn, l1, l2) -> ((bitShiftLeft(CAST(asn, 'UInt128'), 64) + bitShiftLeft(CAST(l1, 'UInt128'), 32)) + CAST(l2, 'UInt128')), `DstLargeCommunities.ASN`, `DstLargeCommunities.LocalData1`, `DstLargeCommunities.LocalData2`)", TransformTo: "arrayMap((asn, l1, l2) -> ((bitShiftLeft(CAST(asn, 'UInt128'), 64) + bitShiftLeft(CAST(l1, 'UInt128'), 32)) + CAST(l2, 'UInt128')), `DstLargeCommunities.ASN`, `DstLargeCommunities.LocalData1`, `DstLargeCommunities.LocalData2`)",
NotSelectable: true,
}, },
{Name: "InIfName", Type: "LowCardinality(String)"}, {Name: "InIfName", Type: "LowCardinality(String)"},
{Name: "InIfDescription", Type: "String", NotSortingKey: true}, {Name: "InIfDescription", Type: "String", NotSortingKey: true},
@@ -121,12 +124,13 @@ END`,
{Name: "EType", Type: "UInt32"}, {Name: "EType", Type: "UInt32"},
{Name: "Proto", Type: "UInt32"}, {Name: "Proto", Type: "UInt32"},
{Name: "SrcPort", Type: "UInt32", MainOnly: true}, {Name: "SrcPort", Type: "UInt32", MainOnly: true},
{Name: "Bytes", Type: "UInt64", NotSortingKey: true}, {Name: "Bytes", Type: "UInt64", NotSortingKey: true, NotSelectable: true},
{Name: "Packets", Type: "UInt64", NotSortingKey: true}, {Name: "Packets", Type: "UInt64", NotSortingKey: true, NotSelectable: true},
{ {
Name: "PacketSize", Name: "PacketSize",
Type: "UInt64", Type: "UInt64",
Alias: "intDiv(Bytes, Packets)", Alias: "intDiv(Bytes, Packets)",
NotSelectable: true,
}, { }, {
Name: "PacketSizeBucket", Name: "PacketSizeBucket",
Type: "LowCardinality(String)", Type: "LowCardinality(String)",

View File

@@ -36,4 +36,7 @@ type Column struct {
GenerateFrom string GenerateFrom string
TransformFrom []Column TransformFrom []Column
TransformTo string TransformTo string
// For the console.
NotSelectable bool
} }

View File

@@ -7,6 +7,8 @@ import (
"net/http" "net/http"
"time" "time"
"akvorado/common/schema"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -50,7 +52,7 @@ func DefaultConfiguration() Configuration {
Start: "6 hours ago", Start: "6 hours ago",
End: "now", End: "now",
Filter: "InIfBoundary = external", Filter: "InIfBoundary = external",
Dimensions: []queryColumn{queryColumnSrcAS}, Dimensions: []queryColumn{"SrcAS"},
Limit: 10, Limit: 10,
}, },
HomepageTopWidgets: []string{"src-as", "src-port", "protocol", "src-country", "etype"}, HomepageTopWidgets: []string{"src-as", "src-port", "protocol", "src-country", "etype"},
@@ -65,5 +67,6 @@ func (c *Component) configHandlerFunc(gc *gin.Context) {
"defaultVisualizeOptions": c.config.DefaultVisualizeOptions, "defaultVisualizeOptions": c.config.DefaultVisualizeOptions,
"dimensionsLimit": c.config.DimensionsLimit, "dimensionsLimit": c.config.DimensionsLimit,
"homepageTopWidgets": c.config.HomepageTopWidgets, "homepageTopWidgets": c.config.HomepageTopWidgets,
"dimensions": schema.Flows.SelectColumns(schema.SkipNotDimension),
}) })
} }

View File

@@ -28,8 +28,57 @@ func TestConfigHandler(t *testing.T) {
"dimensions": []string{"SrcAS"}, "dimensions": []string{"SrcAS"},
"limit": 10, "limit": 10,
}, },
"dimensionsLimit": 50,
"homepageTopWidgets": []string{"src-as", "src-port", "protocol", "src-country", "etype"}, "homepageTopWidgets": []string{"src-as", "src-port", "protocol", "src-country", "etype"},
"dimensionsLimit": 50,
"dimensions": []string{"ExporterAddress",
"ExporterName",
"ExporterGroup",
"ExporterRole",
"ExporterSite",
"ExporterRegion",
"ExporterTenant",
"SrcAddr",
"DstAddr",
"SrcNetPrefix",
"DstNetPrefix",
"SrcAS",
"DstAS",
"SrcNetName",
"DstNetName",
"SrcNetRole",
"DstNetRole",
"SrcNetSite",
"DstNetSite",
"SrcNetRegion",
"DstNetRegion",
"SrcNetTenant",
"DstNetTenant",
"SrcCountry",
"DstCountry",
"DstASPath",
"Dst1stAS",
"Dst2ndAS",
"Dst3rdAS",
"DstCommunities",
"InIfName",
"OutIfName",
"InIfDescription",
"OutIfDescription",
"InIfSpeed",
"OutIfSpeed",
"InIfConnectivity",
"OutIfConnectivity",
"InIfProvider",
"OutIfProvider",
"InIfBoundary",
"OutIfBoundary",
"EType",
"Proto",
"SrcPort",
"DstPort",
"PacketSizeBucket",
"ForwardingStatus",
},
}, },
}, },
}) })

View File

@@ -45,13 +45,12 @@ func ReverseColumnDirection(name string) string {
// in predicate code blocks. // in predicate code blocks.
func (c *current) acceptColumn() (string, error) { func (c *current) acceptColumn() (string, error) {
name := string(c.text) name := string(c.text)
for pair := schema.Flows.Columns.Front(); pair != nil; pair = pair.Next() { for _, columnName := range schema.Flows.Columns.Keys() {
column := pair.Value if strings.EqualFold(name, columnName) {
if strings.EqualFold(name, column.Name) {
if c.globalStore["meta"].(*Meta).ReverseDirection { if c.globalStore["meta"].(*Meta).ReverseDirection {
return ReverseColumnDirection(column.Name), nil return ReverseColumnDirection(columnName), nil
} }
return column.Name, nil return columnName, nil
} }
} }
return "", fmt.Errorf("unknown column %q", name) return "", fmt.Errorf("unknown column %q", name)

View File

@@ -58,7 +58,6 @@ import { dataColor } from "@/utils";
import InputString from "@/components/InputString.vue"; import InputString from "@/components/InputString.vue";
import InputListBox from "@/components/InputListBox.vue"; import InputListBox from "@/components/InputListBox.vue";
import { ServerConfigKey } from "@/components/ServerConfigProvider.vue"; import { ServerConfigKey } from "@/components/ServerConfigProvider.vue";
import fields from "@data/fields.json";
import { isEqual } from "lodash-es"; import { isEqual } from "lodash-es";
const props = withDefaults( const props = withDefaults(
@@ -75,7 +74,7 @@ const emit = defineEmits<{
}>(); }>();
const serverConfiguration = inject(ServerConfigKey)!; const serverConfiguration = inject(ServerConfigKey)!;
const selectedDimensions = ref<Array<typeof dimensions[0]>>([]); const selectedDimensions = ref<Array<typeof dimensions.value[0]>>([]);
const dimensionsError = computed(() => { const dimensionsError = computed(() => {
if (selectedDimensions.value.length < props.minDimensions) { if (selectedDimensions.value.length < props.minDimensions) {
return "At least two dimensions are required"; return "At least two dimensions are required";
@@ -99,25 +98,27 @@ const limitError = computed(() => {
}); });
const hasErrors = computed(() => !!limitError.value || !!dimensionsError.value); const hasErrors = computed(() => !!limitError.value || !!dimensionsError.value);
const dimensions = fields.map((v, idx) => ({ const dimensions = computed(() =>
id: idx + 1, serverConfiguration.value?.dimensions.map((v, idx) => ({
name: v, id: idx + 1,
color: dataColor( name: v,
["Exporter", "Src", "Dst", "In", "Out", ""] color: dataColor(
.map((p) => v.startsWith(p)) ["Exporter", "Src", "Dst", "In", "Out", ""]
.indexOf(true) .map((p) => v.startsWith(p))
), .indexOf(true)
})); ),
}))
);
const removeDimension = (dimension: typeof dimensions[0]) => { const removeDimension = (dimension: typeof dimensions.value[0]) => {
selectedDimensions.value = selectedDimensions.value.filter( selectedDimensions.value = selectedDimensions.value.filter(
(d) => d !== dimension (d) => d !== dimension
); );
}; };
watch( watch(
() => props.modelValue, () => [props.modelValue, dimensions.value] as const,
(value) => { ([value, dimensions]) => {
if (value) { if (value) {
limit.value = value.limit.toString(); limit.value = value.limit.toString();
} }

View File

@@ -30,6 +30,7 @@ type ServerConfig = {
dimensions: string[]; dimensions: string[];
limit: number; limit: number;
}; };
dimensions: string[];
dimensionsLimit: number; dimensionsLimit: number;
homepageTopWidgets: string[]; homepageTopWidgets: string[];
}; };

View File

@@ -1,13 +1,12 @@
{ {
"extends": "@vue/tsconfig/tsconfig.web.json", "extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "data/*.json"], "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": ["./src/*"]
"@data/*": ["./data/*"]
} }
} }
} }

View File

@@ -12,7 +12,6 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)), "@": fileURLToPath(new URL("./src", import.meta.url)),
"@data": fileURLToPath(new URL("./data", import.meta.url)),
}, },
}, },
build: { build: {

View File

@@ -21,8 +21,8 @@ func TestGraphInputReverseDirection(t *testing.T) {
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Points: 100, Points: 100,
Dimensions: []queryColumn{ Dimensions: []queryColumn{
queryColumnExporterName, "ExporterName",
queryColumnInIfProvider, "InIfProvider",
}, },
Filter: queryFilter{ Filter: queryFilter{
Filter: "DstCountry = 'FR' AND SrcCountry = 'US'", Filter: "DstCountry = 'FR' AND SrcCountry = 'US'",
@@ -36,8 +36,8 @@ func TestGraphInputReverseDirection(t *testing.T) {
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Points: 100, Points: 100,
Dimensions: []queryColumn{ Dimensions: []queryColumn{
queryColumnExporterName, "ExporterName",
queryColumnOutIfProvider, "OutIfProvider",
}, },
Filter: queryFilter{ Filter: queryFilter{
Filter: "SrcCountry = 'FR' AND DstCountry = 'US'", Filter: "SrcCountry = 'FR' AND DstCountry = 'US'",
@@ -120,8 +120,8 @@ func TestGraphPreviousPeriod(t *testing.T) {
Start: start, Start: start,
End: end, End: end,
Dimensions: []queryColumn{ Dimensions: []queryColumn{
queryColumnExporterAddress, "ExporterAddress",
queryColumnExporterName, "ExporterName",
}, },
} }
got := input.previousPeriod() got := input.previousPeriod()
@@ -328,8 +328,8 @@ ORDER BY time WITH FILL
Points: 100, Points: 100,
Limit: 20, Limit: 20,
Dimensions: []queryColumn{ Dimensions: []queryColumn{
queryColumnExporterName, "ExporterName",
queryColumnInIfProvider, "InIfProvider",
}, },
Filter: queryFilter{}, Filter: queryFilter{},
Units: "l3bps", Units: "l3bps",
@@ -360,8 +360,8 @@ ORDER BY time WITH FILL
Points: 100, Points: 100,
Limit: 20, Limit: 20,
Dimensions: []queryColumn{ Dimensions: []queryColumn{
queryColumnExporterName, "ExporterName",
queryColumnInIfProvider, "InIfProvider",
}, },
Filter: queryFilter{}, Filter: queryFilter{},
Units: "l3bps", Units: "l3bps",
@@ -409,8 +409,8 @@ ORDER BY time WITH FILL
Points: 100, Points: 100,
Limit: 20, Limit: 20,
Dimensions: []queryColumn{ Dimensions: []queryColumn{
queryColumnExporterName, "ExporterName",
queryColumnInIfProvider, "InIfProvider",
}, },
Filter: queryFilter{}, Filter: queryFilter{},
Units: "l3bps", Units: "l3bps",

View File

@@ -9,50 +9,33 @@ import (
"strings" "strings"
"akvorado/common/helpers" "akvorado/common/helpers"
"akvorado/common/schema"
"akvorado/console/filter" "akvorado/console/filter"
) )
type queryColumn int type queryColumn string
func (qc queryColumn) MarshalText() ([]byte, error) { func (qc queryColumn) MarshalText() ([]byte, error) {
got, ok := queryColumnMap.LoadValue(qc) return []byte(qc), nil
if ok {
return []byte(got), nil
}
return nil, errors.New("unknown field")
} }
func (qc queryColumn) String() string { func (qc queryColumn) String() string {
got, _ := queryColumnMap.LoadValue(qc) return string(qc)
return got
} }
func (qc *queryColumn) UnmarshalText(input []byte) error { func (qc *queryColumn) UnmarshalText(input []byte) error {
got, ok := queryColumnMap.LoadKey(string(input)) name := string(input)
if ok { if column, ok := schema.Flows.Columns.Get(name); ok && !column.NotSelectable {
*qc = got *qc = queryColumn(name)
return nil return nil
} }
return errors.New("unknown field") return errors.New("unknown field")
} }
// queryColumnsRequiringMainTable lists query columns only present in
// the main table. Also check filter/parser.peg.
var queryColumnsRequiringMainTable = map[queryColumn]struct{}{
queryColumnSrcAddr: {},
queryColumnDstAddr: {},
queryColumnSrcNetPrefix: {},
queryColumnDstNetPrefix: {},
queryColumnSrcPort: {},
queryColumnDstPort: {},
queryColumnDstASPath: {},
queryColumnDstCommunities: {},
}
func requireMainTable(qcs []queryColumn, qf queryFilter) bool { func requireMainTable(qcs []queryColumn, qf queryFilter) bool {
if qf.MainTableRequired { if qf.MainTableRequired {
return true return true
} }
for _, qc := range qcs { for _, qc := range qcs {
if _, ok := queryColumnsRequiringMainTable[qc]; ok { if column, ok := schema.Flows.Columns.Get(string(qc)); ok && column.MainOnly {
return true return true
} }
} }
@@ -98,21 +81,21 @@ func (qf *queryFilter) UnmarshalText(input []byte) error {
func (qc queryColumn) toSQLSelect() string { func (qc queryColumn) toSQLSelect() string {
var strValue string var strValue string
switch qc { switch qc {
case queryColumnExporterAddress, queryColumnSrcAddr, queryColumnDstAddr: case "ExporterAddress", "SrcAddr", "DstAddr":
strValue = fmt.Sprintf("replaceRegexpOne(IPv6NumToString(%s), '^::ffff:', '')", qc) strValue = fmt.Sprintf("replaceRegexpOne(IPv6NumToString(%s), '^::ffff:', '')", qc)
case queryColumnSrcAS, queryColumnDstAS, queryColumnDst1stAS, queryColumnDst2ndAS, queryColumnDst3rdAS: case "SrcAS", "DstAS", "Dst1stAS", "Dst2ndAS", "Dst3rdAS":
strValue = fmt.Sprintf(`concat(toString(%s), ': ', dictGetOrDefault('asns', 'name', %s, '???'))`, strValue = fmt.Sprintf(`concat(toString(%s), ': ', dictGetOrDefault('asns', 'name', %s, '???'))`,
qc, qc) qc, qc)
case queryColumnEType: case "EType":
strValue = fmt.Sprintf(`if(EType = %d, 'IPv4', if(EType = %d, 'IPv6', '???'))`, strValue = fmt.Sprintf(`if(EType = %d, 'IPv4', if(EType = %d, 'IPv6', '???'))`,
helpers.ETypeIPv4, helpers.ETypeIPv6) helpers.ETypeIPv4, helpers.ETypeIPv6)
case queryColumnProto: case "Proto":
strValue = `dictGetOrDefault('protocols', 'name', Proto, '???')` strValue = `dictGetOrDefault('protocols', 'name', Proto, '???')`
case queryColumnInIfSpeed, queryColumnOutIfSpeed, queryColumnSrcPort, queryColumnDstPort, queryColumnForwardingStatus, queryColumnInIfBoundary, queryColumnOutIfBoundary: case "InIfSpeed", "OutIfSpeed", "SrcPort", "DstPort", "ForwardingStatus", "InIfBoundary", "OutIfBoundary":
strValue = fmt.Sprintf("toString(%s)", qc) strValue = fmt.Sprintf("toString(%s)", qc)
case queryColumnDstASPath: case "DstASPath":
strValue = `arrayStringConcat(DstASPath, ' ')` strValue = `arrayStringConcat(DstASPath, ' ')`
case queryColumnDstCommunities: case "DstCommunities":
strValue = `arrayStringConcat(arrayConcat(arrayMap(c -> concat(toString(bitShiftRight(c, 16)), ':', toString(bitAnd(c, 0xffff))), DstCommunities), arrayMap(c -> concat(toString(bitAnd(bitShiftRight(c, 64), 0xffffffff)), ':', toString(bitAnd(bitShiftRight(c, 32), 0xffffffff)), ':', toString(bitAnd(c, 0xffffffff))), DstLargeCommunities)), ' ')` strValue = `arrayStringConcat(arrayConcat(arrayMap(c -> concat(toString(bitShiftRight(c, 16)), ':', toString(bitAnd(c, 0xffff))), DstCommunities), arrayMap(c -> concat(toString(bitAnd(bitShiftRight(c, 64), 0xffffffff)), ':', toString(bitAnd(bitShiftRight(c, 32), 0xffffffff)), ':', toString(bitAnd(c, 0xffffffff))), DstLargeCommunities)), ' ')`
default: default:
strValue = qc.String() strValue = qc.String()
@@ -122,19 +105,15 @@ func (qc queryColumn) toSQLSelect() string {
// reverseDirection reverse the direction of a column (src/dst, in/out) // reverseDirection reverse the direction of a column (src/dst, in/out)
func (qc queryColumn) reverseDirection() queryColumn { func (qc queryColumn) reverseDirection() queryColumn {
value, ok := queryColumnMap.LoadKey(filter.ReverseColumnDirection(qc.String())) return queryColumn(filter.ReverseColumnDirection(string(qc)))
if !ok {
panic("unknown reverse column")
}
return value
} }
// fixQueryColumnName fix capitalization of the provided column name // fixQueryColumnName fix capitalization of the provided column name
func fixQueryColumnName(name string) string { func fixQueryColumnName(name string) string {
name = strings.ToLower(name) name = strings.ToLower(name)
for _, target := range queryColumnMap.Values() { for _, k := range schema.Flows.Columns.Keys() {
if strings.ToLower(target) == name { if strings.ToLower(k) == name {
return target return k
} }
} }
return "" return ""

View File

@@ -1,108 +0,0 @@
// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
package console
import "akvorado/common/helpers/bimap"
const (
queryColumnExporterAddress queryColumn = iota + 1
queryColumnExporterName
queryColumnExporterGroup
queryColumnExporterRole
queryColumnExporterSite
queryColumnExporterRegion
queryColumnExporterTenant
queryColumnSrcAS
queryColumnSrcNetName
queryColumnSrcNetRole
queryColumnSrcNetSite
queryColumnSrcNetRegion
queryColumnSrcNetTenant
queryColumnSrcCountry
queryColumnInIfName
queryColumnInIfDescription
queryColumnInIfSpeed
queryColumnInIfConnectivity
queryColumnInIfProvider
queryColumnInIfBoundary
queryColumnEType
queryColumnProto
queryColumnSrcPort
queryColumnSrcAddr
queryColumnSrcNetPrefix
queryColumnDstAS
queryColumnDstASPath
queryColumnDst1stAS
queryColumnDst2ndAS
queryColumnDst3rdAS
queryColumnDstCommunities
queryColumnDstNetName
queryColumnDstNetRole
queryColumnDstNetSite
queryColumnDstNetRegion
queryColumnDstNetTenant
queryColumnDstCountry
queryColumnOutIfName
queryColumnOutIfDescription
queryColumnOutIfSpeed
queryColumnOutIfConnectivity
queryColumnOutIfProvider
queryColumnOutIfBoundary
queryColumnDstAddr
queryColumnDstNetPrefix
queryColumnDstPort
queryColumnForwardingStatus
queryColumnPacketSizeBucket
)
var queryColumnMap = bimap.New(map[queryColumn]string{
queryColumnExporterAddress: "ExporterAddress",
queryColumnExporterName: "ExporterName",
queryColumnExporterGroup: "ExporterGroup",
queryColumnExporterRole: "ExporterRole",
queryColumnExporterSite: "ExporterSite",
queryColumnExporterRegion: "ExporterRegion",
queryColumnExporterTenant: "ExporterTenant",
queryColumnSrcAddr: "SrcAddr",
queryColumnDstAddr: "DstAddr",
queryColumnSrcNetPrefix: "SrcNetPrefix",
queryColumnDstNetPrefix: "DstNetPrefix",
queryColumnSrcAS: "SrcAS",
queryColumnDstAS: "DstAS",
queryColumnDstASPath: "DstASPath",
queryColumnDst1stAS: "Dst1stAS",
queryColumnDst2ndAS: "Dst2ndAS",
queryColumnDst3rdAS: "Dst3rdAS",
queryColumnDstCommunities: "DstCommunities",
queryColumnSrcNetName: "SrcNetName",
queryColumnDstNetName: "DstNetName",
queryColumnSrcNetRole: "SrcNetRole",
queryColumnDstNetRole: "DstNetRole",
queryColumnSrcNetSite: "SrcNetSite",
queryColumnDstNetSite: "DstNetSite",
queryColumnSrcNetRegion: "SrcNetRegion",
queryColumnDstNetRegion: "DstNetRegion",
queryColumnSrcNetTenant: "SrcNetTenant",
queryColumnDstNetTenant: "DstNetTenant",
queryColumnSrcCountry: "SrcCountry",
queryColumnDstCountry: "DstCountry",
queryColumnInIfName: "InIfName",
queryColumnOutIfName: "OutIfName",
queryColumnInIfDescription: "InIfDescription",
queryColumnOutIfDescription: "OutIfDescription",
queryColumnInIfSpeed: "InIfSpeed",
queryColumnOutIfSpeed: "OutIfSpeed",
queryColumnInIfConnectivity: "InIfConnectivity",
queryColumnOutIfConnectivity: "OutIfConnectivity",
queryColumnInIfProvider: "InIfProvider",
queryColumnOutIfProvider: "OutIfProvider",
queryColumnInIfBoundary: "InIfBoundary",
queryColumnOutIfBoundary: "OutIfBoundary",
queryColumnEType: "EType",
queryColumnProto: "Proto",
queryColumnSrcPort: "SrcPort",
queryColumnDstPort: "DstPort",
queryColumnForwardingStatus: "ForwardingStatus",
queryColumnPacketSizeBucket: "PacketSizeBucket",
})

View File

@@ -16,14 +16,14 @@ func TestRequireMainTable(t *testing.T) {
Expected bool Expected bool
}{ }{
{[]queryColumn{}, queryFilter{}, false}, {[]queryColumn{}, queryFilter{}, false},
{[]queryColumn{queryColumnSrcAS}, queryFilter{}, false}, {[]queryColumn{"SrcAS"}, queryFilter{}, false},
{[]queryColumn{queryColumnExporterAddress}, queryFilter{}, false}, {[]queryColumn{"ExporterAddress"}, queryFilter{}, false},
{[]queryColumn{queryColumnSrcPort}, queryFilter{}, true}, {[]queryColumn{"SrcPort"}, queryFilter{}, true},
{[]queryColumn{queryColumnSrcAddr}, queryFilter{}, true}, {[]queryColumn{"SrcAddr"}, queryFilter{}, true},
{[]queryColumn{queryColumnDstPort}, queryFilter{}, true}, {[]queryColumn{"DstPort"}, queryFilter{}, true},
{[]queryColumn{queryColumnDstAddr}, queryFilter{}, true}, {[]queryColumn{"DstAddr"}, queryFilter{}, true},
{[]queryColumn{queryColumnSrcAS, queryColumnDstAddr}, queryFilter{}, true}, {[]queryColumn{"SrcAS", "DstAddr"}, queryFilter{}, true},
{[]queryColumn{queryColumnDstAddr, queryColumnSrcAS}, queryFilter{}, true}, {[]queryColumn{"DstAddr", "SrcAS"}, queryFilter{}, true},
{[]queryColumn{}, queryFilter{MainTableRequired: true}, true}, {[]queryColumn{}, queryFilter{MainTableRequired: true}, true},
} }
for idx, tc := range cases { for idx, tc := range cases {
@@ -34,40 +34,65 @@ func TestRequireMainTable(t *testing.T) {
} }
} }
func TestUnmarshalQueryColumn(t *testing.T) {
cases := []struct {
Input string
Expected string
Error bool
}{
{"DstAddr", "DstAddr", false},
{"TimeReceived", "", true},
{"Nothing", "", true},
}
for _, tc := range cases {
var qc queryColumn
err := qc.UnmarshalText([]byte(tc.Input))
if err != nil && !tc.Error {
t.Fatalf("UnmarshalText(%q) error:\n%+v", tc.Input, err)
}
if err == nil && tc.Error {
t.Fatalf("UnmarshalText(%q) did not error", tc.Input)
}
if diff := helpers.Diff(qc, tc.Expected); diff != "" {
t.Fatalf("UnmarshalText(%q) (-got, +want):\n%s", tc.Input, diff)
}
}
}
func TestQueryColumnSQLSelect(t *testing.T) { func TestQueryColumnSQLSelect(t *testing.T) {
cases := []struct { cases := []struct {
Input queryColumn Input queryColumn
Expected string Expected string
}{ }{
{ {
Input: queryColumnSrcAddr, Input: "SrcAddr",
Expected: `replaceRegexpOne(IPv6NumToString(SrcAddr), '^::ffff:', '')`, Expected: `replaceRegexpOne(IPv6NumToString(SrcAddr), '^::ffff:', '')`,
}, { }, {
Input: queryColumnDstAS, Input: "DstAS",
Expected: `concat(toString(DstAS), ': ', dictGetOrDefault('asns', 'name', DstAS, '???'))`, Expected: `concat(toString(DstAS), ': ', dictGetOrDefault('asns', 'name', DstAS, '???'))`,
}, { }, {
Input: queryColumnDst2ndAS, Input: "Dst2ndAS",
Expected: `concat(toString(Dst2ndAS), ': ', dictGetOrDefault('asns', 'name', Dst2ndAS, '???'))`, Expected: `concat(toString(Dst2ndAS), ': ', dictGetOrDefault('asns', 'name', Dst2ndAS, '???'))`,
}, { }, {
Input: queryColumnProto, Input: "Proto",
Expected: `dictGetOrDefault('protocols', 'name', Proto, '???')`, Expected: `dictGetOrDefault('protocols', 'name', Proto, '???')`,
}, { }, {
Input: queryColumnEType, Input: "EType",
Expected: `if(EType = 2048, 'IPv4', if(EType = 34525, 'IPv6', '???'))`, Expected: `if(EType = 2048, 'IPv4', if(EType = 34525, 'IPv6', '???'))`,
}, { }, {
Input: queryColumnOutIfSpeed, Input: "OutIfSpeed",
Expected: `toString(OutIfSpeed)`, Expected: `toString(OutIfSpeed)`,
}, { }, {
Input: queryColumnExporterName, Input: "ExporterName",
Expected: `ExporterName`, Expected: `ExporterName`,
}, { }, {
Input: queryColumnPacketSizeBucket, Input: "PacketSizeBucket",
Expected: `PacketSizeBucket`, Expected: `PacketSizeBucket`,
}, { }, {
Input: queryColumnDstASPath, Input: "DstASPath",
Expected: `arrayStringConcat(DstASPath, ' ')`, Expected: `arrayStringConcat(DstASPath, ' ')`,
}, { }, {
Input: queryColumnDstCommunities, Input: "DstCommunities",
Expected: `arrayStringConcat(arrayConcat(arrayMap(c -> concat(toString(bitShiftRight(c, 16)), ':', toString(bitAnd(c, 0xffff))), DstCommunities), arrayMap(c -> concat(toString(bitAnd(bitShiftRight(c, 64), 0xffffffff)), ':', toString(bitAnd(bitShiftRight(c, 32), 0xffffffff)), ':', toString(bitAnd(c, 0xffffffff))), DstLargeCommunities)), ' ')`, Expected: `arrayStringConcat(arrayConcat(arrayMap(c -> concat(toString(bitShiftRight(c, 16)), ':', toString(bitAnd(c, 0xffff))), DstCommunities), arrayMap(c -> concat(toString(bitAnd(bitShiftRight(c, 64), 0xffffffff)), ':', toString(bitAnd(bitShiftRight(c, 32), 0xffffffff)), ':', toString(bitAnd(c, 0xffffffff))), DstLargeCommunities)), ' ')`,
}, },
} }

View File

@@ -25,7 +25,7 @@ func TestSankeyQuerySQL(t *testing.T) {
Input: sankeyHandlerInput{ Input: sankeyHandlerInput{
Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC), Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []queryColumn{queryColumnSrcAS, queryColumnExporterName}, Dimensions: []queryColumn{"SrcAS", "ExporterName"},
Limit: 5, Limit: 5,
Filter: queryFilter{}, Filter: queryFilter{},
Units: "l3bps", Units: "l3bps",
@@ -49,7 +49,7 @@ ORDER BY xps DESC
Input: sankeyHandlerInput{ Input: sankeyHandlerInput{
Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC), Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []queryColumn{queryColumnSrcAS, queryColumnExporterName}, Dimensions: []queryColumn{"SrcAS", "ExporterName"},
Limit: 5, Limit: 5,
Filter: queryFilter{}, Filter: queryFilter{},
Units: "l2bps", Units: "l2bps",
@@ -74,7 +74,7 @@ ORDER BY xps DESC
Input: sankeyHandlerInput{ Input: sankeyHandlerInput{
Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC), Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []queryColumn{queryColumnSrcAS, queryColumnExporterName}, Dimensions: []queryColumn{"SrcAS", "ExporterName"},
Limit: 5, Limit: 5,
Filter: queryFilter{}, Filter: queryFilter{},
Units: "pps", Units: "pps",
@@ -98,7 +98,7 @@ ORDER BY xps DESC
Input: sankeyHandlerInput{ Input: sankeyHandlerInput{
Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC), Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []queryColumn{queryColumnSrcAS, queryColumnExporterName}, Dimensions: []queryColumn{"SrcAS", "ExporterName"},
Limit: 10, Limit: 10,
Filter: queryFilter{Filter: "DstCountry = 'FR'"}, Filter: queryFilter{Filter: "DstCountry = 'FR'"},
Units: "l3bps", Units: "l3bps",