diff --git a/.golangci.yml b/.golangci.yml index 272e8014..0c80946c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -39,7 +39,7 @@ linters-settings: - whyNoLint - wrapperFunc gocyclo: - min-complexity: 17 + min-complexity: 30 goheader: values: regexp: diff --git a/docs/examples.md b/docs/examples.md index 61a6a697..82ee80d5 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -33,6 +33,10 @@ It uses the `description` field to filter between primary and secondary NIC. {% include_relative examples/example.md example="all-ethernet-up" %} +## Turn LLDP to true at non ethernet interfaces + +{% include_relative examples/example.md example="non-ethernet-lldp-enabled" %} + ## Create a linux-bridge with all the interfaces matching description {% include_relative examples/example.md example="bridge-interfaces-by-description" %} diff --git a/nmpolicy/internal/ast/types.go b/nmpolicy/internal/ast/types.go index 88051914..23d97608 100644 --- a/nmpolicy/internal/ast/types.go +++ b/nmpolicy/internal/ast/types.go @@ -34,6 +34,7 @@ type Terminal struct { type Node struct { Meta EqFilter *TernaryOperator `json:"eqfilter,omitempty"` + NeFilter *TernaryOperator `json:"nefilter,omitempty"` Replace *TernaryOperator `json:"replace,omitempty"` Path *VariadicOperator `json:"path,omitempty"` Terminal @@ -43,6 +44,9 @@ func (n Node) String() string { if n.EqFilter != nil { return fmt.Sprintf("EqFilter(%s)", *n.EqFilter) } + if n.NeFilter != nil { + return fmt.Sprintf("NeFilter(%s)", *n.NeFilter) + } if n.Replace != nil { return fmt.Sprintf("Replace(%s)", *n.Replace) } diff --git a/nmpolicy/internal/lexer/lexer.go b/nmpolicy/internal/lexer/lexer.go index 5581123b..958d9d98 100644 --- a/nmpolicy/internal/lexer/lexer.go +++ b/nmpolicy/internal/lexer/lexer.go @@ -102,6 +102,8 @@ func (l *lexer) lexCurrentRune() (*Token, error) { return l.lexEqualAs(REPLACE) } else if l.isEqual() { return l.lexEqualAs(EQFILTER) + } else if l.isExclamationMark() { + return l.lexEqualAs(NEFILTER) } else if l.isPlus() { return &Token{l.scn.Position(), MERGE, string(l.scn.Rune())}, nil } else if l.isPipe() { diff --git a/nmpolicy/internal/lexer/lexer_test.go b/nmpolicy/internal/lexer/lexer_test.go index f60f6f55..a196fa2d 100644 --- a/nmpolicy/internal/lexer/lexer_test.go +++ b/nmpolicy/internal/lexer/lexer_test.go @@ -40,6 +40,7 @@ func TestLexer(t *testing.T) { testLinuxBridgeAtDefaultGwScenario(t) } +//nolint:dupl func testBasicExpressions(t *testing.T) { t.Run("basic expressions", func(t *testing.T) { runTest(t, []test{ @@ -97,7 +98,7 @@ func testBasicExpressions(t *testing.T) { {47, lexer.IDENTITY, "doo3"}, {50, lexer.EOF, ""}}, }}, - {" . foo1.dar1:=foo2 . dar2 ... moo3+boo3|doo3 == := :=", expected{tokens: []lexer.Token{ + {" . foo1.dar1:=foo2 . dar2 ... moo3+boo3|doo3 == := := !=", expected{tokens: []lexer.Token{ {1, lexer.DOT, "."}, {3, lexer.IDENTITY, "foo1"}, {7, lexer.DOT, "."}, @@ -117,7 +118,8 @@ func testBasicExpressions(t *testing.T) { {45, lexer.EQFILTER, "=="}, {48, lexer.REPLACE, ":="}, {51, lexer.REPLACE, ":="}, - {52, lexer.EOF, ""}}, + {54, lexer.NEFILTER, "!="}, + {55, lexer.EOF, ""}}, }}, {"foo1.3|foo2", expected{tokens: []lexer.Token{ {0, lexer.IDENTITY, "foo1"}, diff --git a/nmpolicy/internal/lexer/rune.go b/nmpolicy/internal/lexer/rune.go index 66ff8a17..453d353b 100644 --- a/nmpolicy/internal/lexer/rune.go +++ b/nmpolicy/internal/lexer/rune.go @@ -63,6 +63,10 @@ func (l *lexer) isPipe() bool { return l.scn.Rune() == '|' } +func (l *lexer) isExclamationMark() bool { + return l.scn.Rune() == '!' +} + func (l *lexer) isDelimiter() bool { - return l.isEOF() || l.isSpace() || l.isDot() || l.isEqual() || l.isColon() || l.isPlus() || l.isPipe() + return l.isEOF() || l.isSpace() || l.isDot() || l.isEqual() || l.isColon() || l.isPlus() || l.isPipe() || l.isExclamationMark() } diff --git a/nmpolicy/internal/lexer/token.go b/nmpolicy/internal/lexer/token.go index 49fbaf15..4266e6e8 100644 --- a/nmpolicy/internal/lexer/token.go +++ b/nmpolicy/internal/lexer/token.go @@ -31,6 +31,7 @@ const ( PIPE // | REPLACE // := EQFILTER // == + NEFILTER // != MERGE // + operatorsEnd ) @@ -47,6 +48,7 @@ var tokens = []string{ REPLACE: "REPLACE", EQFILTER: "EQFILTER", + NEFILTER: "NEFILTER", MERGE: "MERGE", } diff --git a/nmpolicy/internal/parser/errors.go b/nmpolicy/internal/parser/errors.go index d99e8447..df51cdb0 100644 --- a/nmpolicy/internal/parser/errors.go +++ b/nmpolicy/internal/parser/errors.go @@ -65,6 +65,13 @@ func wrapWithInvalidEqualityFilterError(err error) *parserError { } } +func wrapWithInvalidInequalityFilterError(err error) *parserError { + return &parserError{ + prefix: "invalid inequality filter", + inner: err, + } +} + func wrapWithInvalidReplaceError(err error) *parserError { return &parserError{ prefix: "invalid replace", diff --git a/nmpolicy/internal/parser/parser.go b/nmpolicy/internal/parser/parser.go index e798c01b..51a4cb76 100644 --- a/nmpolicy/internal/parser/parser.go +++ b/nmpolicy/internal/parser/parser.go @@ -78,6 +78,10 @@ func (p *parser) parse() (ast.Node, error) { if err := p.parseEqFilter(); err != nil { return ast.Node{}, err } + } else if p.currentToken().Type == lexer.NEFILTER { + if err := p.parseNeFilter(); err != nil { + return ast.Node{}, err + } } else if p.currentToken().Type == lexer.REPLACE { if err := p.parseReplace(); err != nil { return ast.Node{}, err @@ -227,6 +231,18 @@ func (p *parser) parseEqFilter() error { return nil } +func (p *parser) parseNeFilter() error { + operator := &ast.Node{ + Meta: ast.Meta{Position: p.currentToken().Position}, + NeFilter: &ast.TernaryOperator{}, + } + if err := p.fillInTernaryOperator(operator.NeFilter); err != nil { + return wrapWithInvalidInequalityFilterError(err) + } + p.lastNode = operator + return nil +} + func (p *parser) parseReplace() error { operator := &ast.Node{ Meta: ast.Meta{Position: p.currentToken().Position}, diff --git a/nmpolicy/internal/parser/parser_test.go b/nmpolicy/internal/parser/parser_test.go index 94eeb648..46d805ec 100644 --- a/nmpolicy/internal/parser/parser_test.go +++ b/nmpolicy/internal/parser/parser_test.go @@ -31,6 +31,7 @@ import ( func TestParser(t *testing.T) { testParsePath(t) testParseEqFilter(t) + testParseNeFilter(t) testParseReplace(t) testParseReplaceWithPath(t) testParseCapturePipeReplace(t) @@ -38,6 +39,7 @@ func TestParser(t *testing.T) { testParseBasicFailures(t) testParsePathFailures(t) testParseEqFilterFailure(t) + testParseNeFilterFailure(t) testParseReplaceFailure(t) testParserReuse(t) @@ -188,6 +190,59 @@ func testParseEqFilterFailure(t *testing.T) { runTest(t, tests) } +func testParseNeFilterFailure(t *testing.T) { + var tests = []test{ + expectError(`invalid inequality filter: missing left hand argument +| !=0.0.0.0/0 +| ^`, + fromTokens( + nefilter(), + str("0.0.0.0/0"), + eof(), + ), + ), + expectError(`invalid inequality filter: left hand argument is not a path +| foo!=0.0.0.0/0 +| ...^`, + fromTokens( + str("foo"), + nefilter(), + str("0.0.0.0/0"), + eof(), + ), + ), + expectError(`invalid inequality filter: missing right hand argument +| routes.running.destination!= +| ...........................^`, + fromTokens( + identity("routes"), + dot(), + identity("running"), + dot(), + identity("destination"), + nefilter(), + eof(), + ), + ), + + expectError(`invalid inequality filter: right hand argument is not a string or identity +| routes.running.destination!=!= +| ............................^`, + fromTokens( + identity("routes"), + dot(), + identity("running"), + dot(), + identity("destination"), + nefilter(), + nefilter(), + eof(), + ), + ), + } + runTest(t, tests) +} + func testParseReplaceFailure(t *testing.T) { var tests = []test{ expectError(`invalid replace: missing left hand argument @@ -344,6 +399,83 @@ eqfilter: runTest(t, tests) } +func testParseNeFilter(t *testing.T) { + var tests = []test{ + expectAST(t, ` +pos: 26 +nefilter: +- pos: 0 + identity: currentState +- pos: 0 + path: + - pos: 0 + identity: routes + - pos: 7 + identity: running + - pos: 15 + identity: destination +- pos: 28 + string: 0.0.0.0/0`, + fromTokens( + identity("routes"), + dot(), + identity("running"), + dot(), + identity("destination"), + nefilter(), + str("0.0.0.0/0"), + eof(), + ), + ), + expectAST(t, ` +pos: 33 +nefilter: +- pos: 0 + identity: currentState +- pos: 0 + path: + - pos: 0 + identity: routes + - pos: 7 + identity: running + - pos: 15 + identity: next-hop-interface +- pos: 35 + path: + - pos: 35 + identity: capture + - pos: 43 + identity: default-gw + - pos: 54 + identity: routes + - pos: 61 + number: 0 + - pos: 63 + identity: next-hop-interface +`, + fromTokens( + identity("routes"), + dot(), + identity("running"), + dot(), + identity("next-hop-interface"), + nefilter(), + identity("capture"), + dot(), + identity("default-gw"), + dot(), + identity("routes"), + dot(), + number(0), + dot(), + identity("next-hop-interface"), + eof(), + ), + ), + } + runTest(t, tests) +} + func testParseReplace(t *testing.T) { var tests = []test{ expectAST(t, ` @@ -641,6 +773,10 @@ func eqfilter() lexer.Token { return lexer.Token{Type: lexer.EQFILTER, Literal: "=="} } +func nefilter() lexer.Token { + return lexer.Token{Type: lexer.NEFILTER, Literal: "!="} +} + func replace() lexer.Token { return lexer.Token{Type: lexer.REPLACE, Literal: ":="} } diff --git a/nmpolicy/internal/resolver/errors.go b/nmpolicy/internal/resolver/errors.go index d6aeb56e..7a5a2952 100644 --- a/nmpolicy/internal/resolver/errors.go +++ b/nmpolicy/internal/resolver/errors.go @@ -51,6 +51,10 @@ func wrapWithEqFilterError(err error) error { return fmt.Errorf("eqfilter error: %w", err) } +func wrapWithNeFilterError(err error) error { + return fmt.Errorf("nefilter error: %w", err) +} + func replaceError(format string, a ...interface{}) error { return wrapWithReplaceError(fmt.Errorf(format, a...)) } diff --git a/nmpolicy/internal/resolver/filter.go b/nmpolicy/internal/resolver/filter.go index 7b3af0f5..caeb4600 100644 --- a/nmpolicy/internal/resolver/filter.go +++ b/nmpolicy/internal/resolver/filter.go @@ -23,8 +23,15 @@ import ( "github.com/nmstate/nmpolicy/nmpolicy/internal/ast" ) -func filter(inputState map[string]interface{}, pathSteps ast.VariadicOperator, expectedValue interface{}) (map[string]interface{}, error) { - filtered, err := visitState(newPath(pathSteps), inputState, &filterVisitor{expectedValue: expectedValue}) +func filter( + inputState map[string]interface{}, + pathSteps ast.VariadicOperator, + operator func(interface{}, interface{}) bool, + expectedValue interface{}) (map[string]interface{}, error) { + filtered, err := visitState(newPath(pathSteps), inputState, &filterVisitor{ + operator: operator, + expectedValue: expectedValue, + }) if err != nil { return nil, fmt.Errorf("failed applying operation on the path: %w", err) @@ -40,9 +47,22 @@ func filter(inputState map[string]interface{}, pathSteps ast.VariadicOperator, e } return filteredMap, nil } +func eqfilter( + inputState map[string]interface{}, + pathSteps ast.VariadicOperator, + expectedValue interface{}) (map[string]interface{}, error) { + return filter(inputState, pathSteps, func(lhs, rhs interface{}) bool { return lhs == rhs }, expectedValue) +} +func nefilter( + inputState map[string]interface{}, + pathSteps ast.VariadicOperator, + expectedValue interface{}) (map[string]interface{}, error) { + return filter(inputState, pathSteps, func(lhs, rhs interface{}) bool { return lhs != rhs }, expectedValue) +} type filterVisitor struct { mergeVisitResult bool + operator func(interface{}, interface{}) bool expectedValue interface{} } @@ -61,7 +81,7 @@ func (e filterVisitor) visitLastMap(p path, mapToFilter map[string]interface{}) return nil, pathError(p.currentStep, `type missmatch: the value in the path doesn't match the value to filter. `+ `"%T" != "%T" -> %+v != %+v`, obtainedValue, e.expectedValue, obtainedValue, e.expectedValue) } - if obtainedValue == e.expectedValue { + if e.operator(obtainedValue, e.expectedValue) { return mapToFilter, nil } return nil, nil @@ -109,7 +129,10 @@ func (e filterVisitor) visitSlice(p path, sliceToVisit []interface{}) (interface for _, interfaceToVisit := range sliceToVisit { // Filter only the first slice by forcing "mergeVisitResult" to true // for the the following ones. - visitResult, err := visitState(p, interfaceToVisit, &filterVisitor{mergeVisitResult: true, expectedValue: e.expectedValue}) + visitResult, err := visitState(p, interfaceToVisit, &filterVisitor{ + mergeVisitResult: true, + operator: e.operator, + expectedValue: e.expectedValue}) if err != nil { return nil, err } diff --git a/nmpolicy/internal/resolver/resolver.go b/nmpolicy/internal/resolver/resolver.go index 99f738cc..1a8dd019 100644 --- a/nmpolicy/internal/resolver/resolver.go +++ b/nmpolicy/internal/resolver/resolver.go @@ -114,6 +114,8 @@ func (r *resolver) resolveCaptureEntryName(captureEntryName string) (types.NMSta func (r *resolver) resolveCaptureASTEntry() (types.NMState, error) { if r.currentNode.EqFilter != nil { return r.resolveEqFilter() + } else if r.currentNode.NeFilter != nil { + return r.resolveNeFilter() } else if r.currentNode.Replace != nil { return r.resolveReplace() } else if r.currentNode.Path != nil { @@ -124,13 +126,22 @@ func (r *resolver) resolveCaptureASTEntry() (types.NMState, error) { func (r *resolver) resolveEqFilter() (types.NMState, error) { operator := r.currentNode.EqFilter - filteredState, err := r.resolveTernaryOperator(operator, filter) + filteredState, err := r.resolveTernaryOperator(operator, eqfilter) if err != nil { return nil, wrapWithEqFilterError(err) } return filteredState, nil } +func (r *resolver) resolveNeFilter() (types.NMState, error) { + operator := r.currentNode.NeFilter + filteredState, err := r.resolveTernaryOperator(operator, nefilter) + if err != nil { + return nil, wrapWithNeFilterError(err) + } + return filteredState, nil +} + func (r *resolver) resolveReplace() (types.NMState, error) { operator := r.currentNode.Replace replacedState, err := r.resolveTernaryOperator(operator, replace) @@ -141,7 +152,7 @@ func (r *resolver) resolveReplace() (types.NMState, error) { } func (r *resolver) resolvePathFilter() (types.NMState, error) { - return filter(r.currentState, *r.currentNode.Path, nil) + return eqfilter(r.currentState, *r.currentNode.Path, nil) } func (r *resolver) resolveTernaryOperator(operator *ast.TernaryOperator, diff --git a/nmpolicy/internal/resolver/resolver_test.go b/nmpolicy/internal/resolver/resolver_test.go index 4389e921..e8249b99 100644 --- a/nmpolicy/internal/resolver/resolver_test.go +++ b/nmpolicy/internal/resolver/resolver_test.go @@ -140,6 +140,7 @@ func TestFilter(t *testing.T) { testFilterWithInvalidInputSource(t) testFilterWithInvalidTypeInSource(t) testFilterBadPath(t) + testNeFilter(t) testReplaceCurrentState(t) testReplaceCapturedState(t) @@ -184,6 +185,45 @@ default-gw: }) } +func testNeFilter(t *testing.T) { + t.Run("Filter map with inequality, list on second path identity", func(t *testing.T) { + testToRun := test{ + captureASTPool: ` +non-default-gw: + nefilter: + - pos: 2 + identity: currentState + - pos: 3 + path: + - pos: 4 + identity: routes + - pos: 5 + identity: running + - pos: 6 + identity: destination + - pos: 7 + string: 0.0.0.0/0 +`, + + expectedCapturedStates: ` +non-default-gw: + state: + routes: + running: + - destination: 1.1.1.0/24 + next-hop-address: 192.168.100.1 + next-hop-interface: eth1 + table-id: 254 + - destination: 2.2.2.0/24 + next-hop-address: 192.168.200.1 + next-hop-interface: eth2 + table-id: 254 +`, + } + runTest(t, &testToRun) + }) +} + func testFilterMapListOnFirstPathIdentity(t *testing.T) { t.Run("Filter map, list on first path identity", func(t *testing.T) { testToRun := test{