mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-01-18 03:28:19 +01:00
network enhanecment / refactor (#361)
* chore(network): improve connectivity check * refactor(network): rewrite network and timesync component * feat(display): show cloud connection status * chore: change logging verbosity * chore(websecure): update log message * fix(ota): validate root certificate when downloading update * feat(ui): add network settings tab * fix(display): cloud connecting animation * fix: golintci issues * feat: add network settings tab * feat(timesync): query servers in parallel * refactor(network): move to internal/network package * feat(timesync): add metrics * refactor(log): move log to internal/logging package * refactor(mdms): move mdns to internal/mdns package * feat(developer): add pprof endpoint * feat(logging): add a simple logging streaming endpoint * fix(mdns): do not start mdns until network is up * feat(network): allow users to update network settings from ui * fix(network): handle errors when net.IPAddr is nil * fix(mdns): scopedLogger SIGSEGV * fix(dhcp): watch directory instead of file to catch fsnotify.Create event * refactor(nbd): move platform-specific code to different files * refactor(native): move platform-specific code to different files * chore: fix linter issues * chore(dev_deploy): allow to override PION_LOG_TRACE
This commit is contained in:
381
internal/confparser/confparser.go
Normal file
381
internal/confparser/confparser.go
Normal file
@@ -0,0 +1,381 @@
|
||||
package confparser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/guregu/null/v6"
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
type FieldConfig struct {
|
||||
Name string
|
||||
Required bool
|
||||
RequiredIf map[string]interface{}
|
||||
OneOf []string
|
||||
ValidateTypes []string
|
||||
Defaults interface{}
|
||||
IsEmpty bool
|
||||
CurrentValue interface{}
|
||||
TypeString string
|
||||
Delegated bool
|
||||
shouldUpdateValue bool
|
||||
}
|
||||
|
||||
func SetDefaultsAndValidate(config interface{}) error {
|
||||
return setDefaultsAndValidate(config, true)
|
||||
}
|
||||
|
||||
func setDefaultsAndValidate(config interface{}, isRoot bool) error {
|
||||
// first we need to check if the config is a pointer
|
||||
if reflect.TypeOf(config).Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("config is not a pointer")
|
||||
}
|
||||
|
||||
// now iterate over the lease struct and set the values
|
||||
configType := reflect.TypeOf(config).Elem()
|
||||
configValue := reflect.ValueOf(config).Elem()
|
||||
|
||||
fields := make(map[string]FieldConfig)
|
||||
|
||||
for i := 0; i < configType.NumField(); i++ {
|
||||
field := configType.Field(i)
|
||||
fieldValue := configValue.Field(i)
|
||||
|
||||
defaultValue := field.Tag.Get("default")
|
||||
|
||||
fieldType := field.Type.String()
|
||||
|
||||
fieldConfig := FieldConfig{
|
||||
Name: field.Name,
|
||||
OneOf: splitString(field.Tag.Get("one_of")),
|
||||
ValidateTypes: splitString(field.Tag.Get("validate_type")),
|
||||
RequiredIf: make(map[string]interface{}),
|
||||
CurrentValue: fieldValue.Interface(),
|
||||
IsEmpty: false,
|
||||
TypeString: fieldType,
|
||||
}
|
||||
|
||||
// check if the field is required
|
||||
required := field.Tag.Get("required")
|
||||
if required != "" {
|
||||
requiredBool, _ := strconv.ParseBool(required)
|
||||
fieldConfig.Required = requiredBool
|
||||
}
|
||||
|
||||
var canUseOneOff = false
|
||||
|
||||
// use switch to get the type
|
||||
switch fieldValue.Interface().(type) {
|
||||
case string, null.String:
|
||||
if defaultValue != "" {
|
||||
fieldConfig.Defaults = defaultValue
|
||||
}
|
||||
canUseOneOff = true
|
||||
case []string:
|
||||
if defaultValue != "" {
|
||||
fieldConfig.Defaults = strings.Split(defaultValue, ",")
|
||||
}
|
||||
canUseOneOff = true
|
||||
case int, null.Int:
|
||||
if defaultValue != "" {
|
||||
defaultValueInt, err := strconv.Atoi(defaultValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid default value for field `%s`: %s", field.Name, defaultValue)
|
||||
}
|
||||
|
||||
fieldConfig.Defaults = defaultValueInt
|
||||
}
|
||||
case bool, null.Bool:
|
||||
if defaultValue != "" {
|
||||
defaultValueBool, err := strconv.ParseBool(defaultValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid default value for field `%s`: %s", field.Name, defaultValue)
|
||||
}
|
||||
|
||||
fieldConfig.Defaults = defaultValueBool
|
||||
}
|
||||
default:
|
||||
if defaultValue != "" {
|
||||
return fmt.Errorf("field `%s` cannot use default value: unsupported type: %s", field.Name, fieldType)
|
||||
}
|
||||
|
||||
// check if it's a pointer
|
||||
if fieldValue.Kind() == reflect.Ptr {
|
||||
// check if the pointer is nil
|
||||
if fieldValue.IsNil() {
|
||||
fieldConfig.IsEmpty = true
|
||||
} else {
|
||||
fieldConfig.CurrentValue = fieldValue.Elem().Addr()
|
||||
fieldConfig.Delegated = true
|
||||
}
|
||||
} else {
|
||||
fieldConfig.Delegated = true
|
||||
}
|
||||
}
|
||||
|
||||
// now check if the field is nullable interface
|
||||
switch fieldValue.Interface().(type) {
|
||||
case null.String:
|
||||
if fieldValue.Interface().(null.String).IsZero() {
|
||||
fieldConfig.IsEmpty = true
|
||||
}
|
||||
case null.Int:
|
||||
if fieldValue.Interface().(null.Int).IsZero() {
|
||||
fieldConfig.IsEmpty = true
|
||||
}
|
||||
case null.Bool:
|
||||
if fieldValue.Interface().(null.Bool).IsZero() {
|
||||
fieldConfig.IsEmpty = true
|
||||
}
|
||||
case []string:
|
||||
if len(fieldValue.Interface().([]string)) == 0 {
|
||||
fieldConfig.IsEmpty = true
|
||||
}
|
||||
}
|
||||
|
||||
// now check if the field has required_if
|
||||
requiredIf := field.Tag.Get("required_if")
|
||||
if requiredIf != "" {
|
||||
requiredIfParts := strings.Split(requiredIf, ",")
|
||||
for _, part := range requiredIfParts {
|
||||
partVal := strings.SplitN(part, "=", 2)
|
||||
if len(partVal) != 2 {
|
||||
return fmt.Errorf("invalid required_if for field `%s`: %s", field.Name, requiredIf)
|
||||
}
|
||||
|
||||
fieldConfig.RequiredIf[partVal[0]] = partVal[1]
|
||||
}
|
||||
}
|
||||
|
||||
// check if the field can use one_of
|
||||
if !canUseOneOff && len(fieldConfig.OneOf) > 0 {
|
||||
return fmt.Errorf("field `%s` cannot use one_of: unsupported type: %s", field.Name, fieldType)
|
||||
}
|
||||
|
||||
fields[field.Name] = fieldConfig
|
||||
}
|
||||
|
||||
if err := validateFields(config, fields); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateFields(config interface{}, fields map[string]FieldConfig) error {
|
||||
// now we can start to validate the fields
|
||||
for _, fieldConfig := range fields {
|
||||
if err := fieldConfig.validate(fields); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fieldConfig.populate(config)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FieldConfig) validate(fields map[string]FieldConfig) error {
|
||||
var required bool
|
||||
var err error
|
||||
|
||||
if required, err = f.validateRequired(fields); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if the field needs to be updated and set defaults if needed
|
||||
if err := f.checkIfFieldNeedsUpdate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// then we can check if the field is one_of
|
||||
if err := f.validateOneOf(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// and validate the type
|
||||
if err := f.validateField(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if the field is delegated, we need to validate the nested field
|
||||
// but before that, let's check if the field is required
|
||||
if required && f.Delegated {
|
||||
if err := setDefaultsAndValidate(f.CurrentValue.(reflect.Value).Interface(), false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FieldConfig) populate(config interface{}) {
|
||||
// update the field if it's not empty
|
||||
if !f.shouldUpdateValue {
|
||||
return
|
||||
}
|
||||
|
||||
reflect.ValueOf(config).Elem().FieldByName(f.Name).Set(reflect.ValueOf(f.CurrentValue))
|
||||
}
|
||||
|
||||
func (f *FieldConfig) checkIfFieldNeedsUpdate() error {
|
||||
// populate the field if it's empty and has a default value
|
||||
if f.IsEmpty && f.Defaults != nil {
|
||||
switch f.CurrentValue.(type) {
|
||||
case null.String:
|
||||
f.CurrentValue = null.StringFrom(f.Defaults.(string))
|
||||
case null.Int:
|
||||
f.CurrentValue = null.IntFrom(int64(f.Defaults.(int)))
|
||||
case null.Bool:
|
||||
f.CurrentValue = null.BoolFrom(f.Defaults.(bool))
|
||||
case string:
|
||||
f.CurrentValue = f.Defaults.(string)
|
||||
case int:
|
||||
f.CurrentValue = f.Defaults.(int)
|
||||
case bool:
|
||||
f.CurrentValue = f.Defaults.(bool)
|
||||
case []string:
|
||||
f.CurrentValue = f.Defaults.([]string)
|
||||
default:
|
||||
return fmt.Errorf("field `%s` cannot use default value: unsupported type: %s", f.Name, f.TypeString)
|
||||
}
|
||||
|
||||
f.shouldUpdateValue = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FieldConfig) validateRequired(fields map[string]FieldConfig) (bool, error) {
|
||||
var required = f.Required
|
||||
|
||||
// if the field is not required, we need to check if it's required_if
|
||||
if !required && len(f.RequiredIf) > 0 {
|
||||
for key, value := range f.RequiredIf {
|
||||
// check if the field's result matches the required_if
|
||||
// right now we only support string and int
|
||||
requiredField, ok := fields[key]
|
||||
if !ok {
|
||||
return required, fmt.Errorf("required_if field `%s` not found", key)
|
||||
}
|
||||
|
||||
switch requiredField.CurrentValue.(type) {
|
||||
case string:
|
||||
if requiredField.CurrentValue.(string) == value.(string) {
|
||||
required = true
|
||||
}
|
||||
case int:
|
||||
if requiredField.CurrentValue.(int) == value.(int) {
|
||||
required = true
|
||||
}
|
||||
case null.String:
|
||||
if !requiredField.CurrentValue.(null.String).IsZero() &&
|
||||
requiredField.CurrentValue.(null.String).String == value.(string) {
|
||||
required = true
|
||||
}
|
||||
case null.Int:
|
||||
if !requiredField.CurrentValue.(null.Int).IsZero() &&
|
||||
requiredField.CurrentValue.(null.Int).Int64 == value.(int64) {
|
||||
required = true
|
||||
}
|
||||
}
|
||||
|
||||
// if the field is required, we can break the loop
|
||||
// because we only need one of the required_if fields to be true
|
||||
if required {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if required && f.IsEmpty {
|
||||
return false, fmt.Errorf("field `%s` is required", f.Name)
|
||||
}
|
||||
|
||||
return required, nil
|
||||
}
|
||||
|
||||
func checkIfSliceContains(slice []string, one_of []string) bool {
|
||||
for _, oneOf := range one_of {
|
||||
if slices.Contains(slice, oneOf) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *FieldConfig) validateOneOf() error {
|
||||
if len(f.OneOf) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var val []string
|
||||
switch f.CurrentValue.(type) {
|
||||
case string:
|
||||
val = []string{f.CurrentValue.(string)}
|
||||
case null.String:
|
||||
val = []string{f.CurrentValue.(null.String).String}
|
||||
case []string:
|
||||
// let's validate the value here
|
||||
val = f.CurrentValue.([]string)
|
||||
default:
|
||||
return fmt.Errorf("field `%s` cannot use one_of: unsupported type: %s", f.Name, f.TypeString)
|
||||
}
|
||||
|
||||
if !checkIfSliceContains(val, f.OneOf) {
|
||||
return fmt.Errorf(
|
||||
"field `%s` is not one of the allowed values: %s, current value: %s",
|
||||
f.Name,
|
||||
strings.Join(f.OneOf, ", "),
|
||||
strings.Join(val, ", "),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FieldConfig) validateField() error {
|
||||
if len(f.ValidateTypes) == 0 || f.IsEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
val, err := toString(f.CurrentValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("field `%s` cannot use validate_type: %s", f.Name, err)
|
||||
}
|
||||
|
||||
if val == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, validateType := range f.ValidateTypes {
|
||||
switch validateType {
|
||||
case "ipv4":
|
||||
if net.ParseIP(val).To4() == nil {
|
||||
return fmt.Errorf("field `%s` is not a valid IPv4 address: %s", f.Name, val)
|
||||
}
|
||||
case "ipv6":
|
||||
if net.ParseIP(val).To16() == nil {
|
||||
return fmt.Errorf("field `%s` is not a valid IPv6 address: %s", f.Name, val)
|
||||
}
|
||||
case "hwaddr":
|
||||
if _, err := net.ParseMAC(val); err != nil {
|
||||
return fmt.Errorf("field `%s` is not a valid MAC address: %s", f.Name, val)
|
||||
}
|
||||
case "hostname":
|
||||
if _, err := idna.Lookup.ToASCII(val); err != nil {
|
||||
return fmt.Errorf("field `%s` is not a valid hostname: %s", f.Name, val)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
100
internal/confparser/confparser_test.go
Normal file
100
internal/confparser/confparser_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package confparser
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/guregu/null/v6"
|
||||
)
|
||||
|
||||
type testIPv6Address struct { //nolint:unused
|
||||
Address net.IP `json:"address"`
|
||||
Prefix net.IPNet `json:"prefix"`
|
||||
ValidLifetime *time.Time `json:"valid_lifetime"`
|
||||
PreferredLifetime *time.Time `json:"preferred_lifetime"`
|
||||
Scope int `json:"scope"`
|
||||
}
|
||||
|
||||
type testIPv4StaticConfig struct {
|
||||
Address null.String `json:"address" validate_type:"ipv4" required:"true"`
|
||||
Netmask null.String `json:"netmask" validate_type:"ipv4" required:"true"`
|
||||
Gateway null.String `json:"gateway" validate_type:"ipv4" required:"true"`
|
||||
DNS []string `json:"dns" validate_type:"ipv4" required:"true"`
|
||||
}
|
||||
|
||||
type testIPv6StaticConfig struct {
|
||||
Address null.String `json:"address" validate_type:"ipv6" required:"true"`
|
||||
Prefix null.String `json:"prefix" validate_type:"ipv6" required:"true"`
|
||||
Gateway null.String `json:"gateway" validate_type:"ipv6" required:"true"`
|
||||
DNS []string `json:"dns" validate_type:"ipv6" required:"true"`
|
||||
}
|
||||
type testNetworkConfig struct {
|
||||
Hostname null.String `json:"hostname,omitempty"`
|
||||
Domain null.String `json:"domain,omitempty"`
|
||||
|
||||
IPv4Mode null.String `json:"ipv4_mode" one_of:"dhcp,static,disabled" default:"dhcp"`
|
||||
IPv4Static *testIPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"`
|
||||
|
||||
IPv6Mode null.String `json:"ipv6_mode" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
|
||||
IPv6Static *testIPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
|
||||
|
||||
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"`
|
||||
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
||||
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
||||
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"`
|
||||
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
|
||||
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
|
||||
}
|
||||
|
||||
func TestValidateConfig(t *testing.T) {
|
||||
config := &testNetworkConfig{}
|
||||
|
||||
err := SetDefaultsAndValidate(config)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateIPv4StaticConfigRequired(t *testing.T) {
|
||||
config := &testNetworkConfig{
|
||||
IPv4Static: &testIPv4StaticConfig{
|
||||
Address: null.StringFrom("192.168.1.1"),
|
||||
Gateway: null.StringFrom("192.168.1.1"),
|
||||
},
|
||||
}
|
||||
|
||||
err := SetDefaultsAndValidate(config)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateIPv4StaticConfigRequiredIf(t *testing.T) {
|
||||
config := &testNetworkConfig{
|
||||
IPv4Mode: null.StringFrom("static"),
|
||||
}
|
||||
|
||||
err := SetDefaultsAndValidate(config)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateIPv4StaticConfigValidateType(t *testing.T) {
|
||||
config := &testNetworkConfig{
|
||||
IPv4Static: &testIPv4StaticConfig{
|
||||
Address: null.StringFrom("X"),
|
||||
Netmask: null.StringFrom("255.255.255.0"),
|
||||
Gateway: null.StringFrom("192.168.1.1"),
|
||||
DNS: []string{"8.8.8.8", "8.8.4.4"},
|
||||
},
|
||||
IPv4Mode: null.StringFrom("static"),
|
||||
}
|
||||
|
||||
err := SetDefaultsAndValidate(config)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
}
|
||||
28
internal/confparser/utils.go
Normal file
28
internal/confparser/utils.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package confparser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/guregu/null/v6"
|
||||
)
|
||||
|
||||
func splitString(s string) []string {
|
||||
if s == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return strings.Split(s, ",")
|
||||
}
|
||||
|
||||
func toString(v interface{}) (string, error) {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return v, nil
|
||||
case null.String:
|
||||
return v.String, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unsupported type: %s", reflect.TypeOf(v))
|
||||
}
|
||||
Reference in New Issue
Block a user