osbuild2: add udev.rules stage
The `org.osbuild.udev.rules` stage creates custom udev rules files. This is a full implementation of the stage and includes information about valid operators and keys. A small test suit to test the basic functionality and validation is included.
This commit is contained in:
parent
13c79294b6
commit
e08fd989ed
2 changed files with 441 additions and 0 deletions
257
internal/osbuild2/udev_rules_stage.go
Normal file
257
internal/osbuild2/udev_rules_stage.go
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
package osbuild2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type OpType int
|
||||
|
||||
const (
|
||||
OpMatch OpType = 0
|
||||
OpAssign OpType = 1
|
||||
)
|
||||
|
||||
var ops = map[string]OpType{
|
||||
"=": OpAssign,
|
||||
"+=": OpAssign,
|
||||
"-=": OpAssign,
|
||||
":=": OpAssign,
|
||||
"==": OpMatch,
|
||||
"!=": OpMatch,
|
||||
}
|
||||
|
||||
type KeyType struct {
|
||||
Arg bool
|
||||
Assign bool
|
||||
Match bool
|
||||
}
|
||||
|
||||
var keys = map[string]KeyType{
|
||||
"ACTION": {Match: true},
|
||||
"DEVPATH": {Match: true},
|
||||
"KERNEL": {Match: true},
|
||||
"KERNELS": {Match: true},
|
||||
"NAME": {Match: true, Assign: true},
|
||||
"SYMLINK": {Match: true, Assign: true},
|
||||
"SUBSYSTEM": {Match: true},
|
||||
"SUBSYSTEMS": {Match: true},
|
||||
"DRIVER": {Match: true},
|
||||
"DRIVERS": {Match: true},
|
||||
"TAG": {Match: true, Assign: true},
|
||||
"TAGS": {Match: true},
|
||||
"PROGRAM": {Match: true},
|
||||
"RESULT": {Match: true},
|
||||
|
||||
"ATTR": {Arg: true, Match: true, Assign: true},
|
||||
"ATTRS": {Arg: true, Match: true},
|
||||
"SYSCTL": {Arg: true, Match: true, Assign: true},
|
||||
"ENV": {Arg: true, Match: true, Assign: true},
|
||||
"CONST": {Arg: true, Match: true},
|
||||
"TEST": {Arg: true, Match: true},
|
||||
|
||||
"OWNER": {Assign: true},
|
||||
"GROUP": {Assign: true},
|
||||
"MODE": {Assign: true},
|
||||
"LABEL": {Assign: true},
|
||||
"GOTO": {Assign: true},
|
||||
"OPTIONS": {Assign: true},
|
||||
|
||||
"SECLABEL": {Arg: true, Assign: true},
|
||||
"RUN": {Arg: true, Assign: true},
|
||||
"IMPORT": {Arg: true, Assign: true},
|
||||
}
|
||||
|
||||
func validate_op(key, op, val, arg string) error {
|
||||
if key == "" {
|
||||
return fmt.Errorf("key is required")
|
||||
}
|
||||
if op == "" {
|
||||
return fmt.Errorf("operator is required")
|
||||
}
|
||||
if val == "" {
|
||||
return fmt.Errorf("value is required")
|
||||
}
|
||||
|
||||
keyInfo, ok := keys[key]
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("key '%s' is unknown", key)
|
||||
}
|
||||
|
||||
if keyInfo.Arg && arg == "" {
|
||||
return fmt.Errorf("arg is required for key '%s'", key)
|
||||
}
|
||||
|
||||
opType, ok := ops[op]
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("'%s' operator is not supported", op)
|
||||
}
|
||||
|
||||
if (opType == OpMatch && !keyInfo.Match) ||
|
||||
(opType == OpAssign && !keyInfo.Assign) {
|
||||
return fmt.Errorf("key '%s' does not support '%s'", key, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UdevRulesStageOptions struct {
|
||||
Filename string `json:"filename"`
|
||||
Rules UdevRules `json:"rules"`
|
||||
}
|
||||
|
||||
func (UdevRulesStageOptions) isStageOptions() {}
|
||||
|
||||
func (o UdevRulesStageOptions) validate() error {
|
||||
if len(o.Rules) == 0 {
|
||||
return fmt.Errorf("at least one rule is required")
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`^[.\/\w\-_]{1,250}.rules$`)
|
||||
if !re.MatchString(o.Filename) {
|
||||
return fmt.Errorf("udev.rules filename '%q' doesn't conform to schema '%s'", o.Filename, re.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewUdevRulesStage(options *UdevRulesStageOptions) *Stage {
|
||||
if err := options.validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &Stage{
|
||||
Type: "org.osbuild.udev.rules",
|
||||
Options: options,
|
||||
}
|
||||
}
|
||||
|
||||
type UdevRules []UdevRule
|
||||
|
||||
type UdevRule interface {
|
||||
isUdevRule()
|
||||
}
|
||||
|
||||
// Comments
|
||||
type UdevRuleComment struct {
|
||||
Comment []string `json:"comment"`
|
||||
}
|
||||
|
||||
func (UdevRuleComment) isUdevRule() {}
|
||||
|
||||
func NewUdevRuleComment(comment []string) UdevRule {
|
||||
return UdevRuleComment{
|
||||
Comment: comment,
|
||||
}
|
||||
}
|
||||
|
||||
// Match and Assignments
|
||||
|
||||
type UdevOps []UdevOp
|
||||
|
||||
func (UdevOps) isUdevRule() {}
|
||||
|
||||
type UdevOp interface {
|
||||
isUdevOp()
|
||||
validate() error
|
||||
}
|
||||
|
||||
type UdevRuleKey interface {
|
||||
isUdevRuleKey()
|
||||
}
|
||||
|
||||
type UdevRuleKeySimple struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
func (UdevRuleKeySimple) isUdevRuleKey() {}
|
||||
|
||||
type UdevRuleKeyArg struct {
|
||||
Name string `json:"name"`
|
||||
Arg string `json:"arg"`
|
||||
}
|
||||
|
||||
func (UdevRuleKeyArg) isUdevRuleKey() {}
|
||||
|
||||
type UdevOpSimple struct {
|
||||
Key string `json:"key"`
|
||||
Op string `json:"op"`
|
||||
Value string `json:"val"`
|
||||
}
|
||||
|
||||
func (o UdevOpSimple) validate() error {
|
||||
err := validate_op(o.Key, o.Op, o.Value, "")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("invalid op: %v", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (UdevOpSimple) isUdevOp() {}
|
||||
|
||||
type UdevOpArg struct {
|
||||
Key UdevRuleKeyArg `json:"key"`
|
||||
Op string `json:"op"`
|
||||
Value string `json:"val"`
|
||||
}
|
||||
|
||||
func (UdevOpArg) isUdevOp() {}
|
||||
|
||||
func (o UdevOpArg) validate() error {
|
||||
err := validate_op(o.Key.Name, o.Op, o.Value, o.Key.Arg)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("invalid op: %v", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// UdevKV is a helper struct that in order to be able to create a UdevRule
|
||||
// more compactly
|
||||
type UdevKV struct {
|
||||
K string // Key, e.g. "ENV"
|
||||
A string // Argument for the key, MANAGED, in `ENV{MANAGED}`
|
||||
O string // Operator, e.g. "="
|
||||
V string // Value, e.g. "1"
|
||||
}
|
||||
|
||||
//NewUdevRule creates a new UdevRule from a list of UdevKV
|
||||
//helper structs. A UdevOpSimple or a UdevOpArg is created
|
||||
//depending on the value of the `A` field. The result is
|
||||
//validated and the function will panic if validation fails.
|
||||
func NewUdevRule(ops []UdevKV) UdevRule {
|
||||
res := make(UdevOps, 0, len(ops))
|
||||
|
||||
for _, o := range ops {
|
||||
|
||||
var op UdevOp
|
||||
|
||||
if o.A == "" {
|
||||
op = UdevOpSimple{
|
||||
Key: o.K,
|
||||
Op: o.O,
|
||||
Value: o.V,
|
||||
}
|
||||
} else {
|
||||
op = UdevOpArg{
|
||||
Key: UdevRuleKeyArg{
|
||||
Name: o.K,
|
||||
Arg: o.A,
|
||||
},
|
||||
Op: o.O,
|
||||
Value: o.V,
|
||||
}
|
||||
}
|
||||
|
||||
if err := op.validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
res = append(res, op)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
184
internal/osbuild2/udev_rules_stage_test.go
Normal file
184
internal/osbuild2/udev_rules_stage_test.go
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
package osbuild2
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewUdevRulesStage(t *testing.T) {
|
||||
stage := NewUdevRulesStage(
|
||||
&UdevRulesStageOptions{
|
||||
Filename: "/etc/udev/udev.rules",
|
||||
Rules: UdevRules{
|
||||
NewUdevRuleComment([]string{"This is a comment"}),
|
||||
NewUdevRule(
|
||||
[]UdevKV{
|
||||
{K: "ACTION", O: "==", V: "add"},
|
||||
{K: "ENV", A: "OSBUILD", O: "=", V: "1"},
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
want := &Stage{
|
||||
Type: "org.osbuild.udev.rules",
|
||||
Options: &UdevRulesStageOptions{
|
||||
Filename: "/etc/udev/udev.rules",
|
||||
Rules: UdevRules{
|
||||
UdevRuleComment{
|
||||
Comment: []string{"This is a comment"},
|
||||
},
|
||||
UdevOps{
|
||||
UdevOpSimple{
|
||||
Key: "ACTION",
|
||||
Op: "==",
|
||||
Value: "add",
|
||||
},
|
||||
UdevOpArg{
|
||||
Key: UdevRuleKeyArg{
|
||||
Name: "ENV",
|
||||
Arg: "OSBUILD",
|
||||
},
|
||||
Op: "=",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, stage, want)
|
||||
}
|
||||
|
||||
func TestNewUdevRulesStageValidate(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
filename string
|
||||
rules []UdevRule
|
||||
}{
|
||||
{
|
||||
name: "no filename",
|
||||
rules: UdevRules{
|
||||
UdevRuleComment{
|
||||
Comment: []string{
|
||||
"This is a comment",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrong filename",
|
||||
filename: "/etc/udev/udev.conf",
|
||||
rules: UdevRules{
|
||||
UdevRuleComment{
|
||||
Comment: []string{
|
||||
"This is a comment",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing rules",
|
||||
filename: "/etc/udev/rules.d/osbuild.rules",
|
||||
},
|
||||
{
|
||||
name: "empty rules",
|
||||
filename: "/etc/udev/rules.d/osbuild.rules",
|
||||
rules: UdevRules{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Panics(func() {
|
||||
NewUdevRulesStage(&UdevRulesStageOptions{
|
||||
Filename: tt.filename,
|
||||
Rules: tt.rules,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUdevRuleValidation(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rule UdevKV
|
||||
}{
|
||||
{
|
||||
name: "no key",
|
||||
rule: UdevKV{
|
||||
O: "==",
|
||||
V: "add",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no op",
|
||||
rule: UdevKV{
|
||||
K: "ACTION",
|
||||
V: "add",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no value",
|
||||
rule: UdevKV{
|
||||
K: "ACTION",
|
||||
O: "==",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unknown key",
|
||||
rule: UdevKV{
|
||||
K: "ACHILLEAS",
|
||||
O: "==",
|
||||
V: "RE GOMBARE",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing arg",
|
||||
rule: UdevKV{
|
||||
K: "ENV",
|
||||
O: "==",
|
||||
V: "RE GOMBARE",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unknown op",
|
||||
rule: UdevKV{
|
||||
K: "ENV",
|
||||
O: "?",
|
||||
V: "RE GOMBARE",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "false assign",
|
||||
rule: UdevKV{
|
||||
K: "ACTION",
|
||||
O: "=",
|
||||
V: "RE GOMBARE",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "false match",
|
||||
rule: UdevKV{
|
||||
K: "OPTIONS",
|
||||
O: "==",
|
||||
V: "RE GOMBARE",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Panics(func() {
|
||||
NewUdevRule([]UdevKV{tt.rule})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue