package utils import ( "database/sql" "database/sql/driver" "errors" "fmt" "net/netip" "strings" "edge-infra.dev/pkg/edge/api/graph/model" "edge-infra.dev/pkg/lib/networkvalidator" "edge-infra.dev/pkg/sds/lib/set" ) type NullInet struct { Inet model.InetType Valid bool // Valid is true if Inet is not NULL } // Scan implements the Scanner interface. func (ni *NullInet) Scan(value any) error { if value == nil { ni.Valid = false return nil } ni.Valid = true return ni.Inet.UnmarshalGQL(value) } // Value implements the driver Valuer interface. func (ni NullInet) Value() (driver.Value, error) { if !ni.Valid { return nil, nil } return ni.Inet, nil } type NullIface struct { TerminalInterfaceID sql.NullString MacAddress sql.NullString Dhcp4 sql.NullBool Dhcp6 sql.NullBool Gateway4 sql.NullString Gateway6 sql.NullString TerminalID sql.NullString } type NullAddress struct { TerminalAddressID sql.NullString IP sql.NullString PrefixLen sql.NullInt16 Family NullInet TerminalInterfaceID sql.NullString } func CreateTerminalModel(terminalID string, clusterEdgeID string, lane *string, terminalRole model.TerminalRoleType, terminalClass *model.TerminalClassType, terminalDiscoverDisks *model.TerminalDiscoverDisksType, terminalBootDisk *string, clusterName string, hostname string, existingEfiPart *string, swapEnabled bool) model.Terminal { discoverDisks := model.TerminalDiscoverDisksTypeEmpty if terminalDiscoverDisks == nil { terminalDiscoverDisks = &discoverDisks } return model.Terminal{ TerminalID: terminalID, Lane: lane, Role: terminalRole, Class: terminalClass, DiscoverDisks: terminalDiscoverDisks, BootDisk: terminalBootDisk, ClusterEdgeID: clusterEdgeID, ClusterName: clusterName, Hostname: hostname, ExistingEfiPart: existingEfiPart, SwapEnabled: swapEnabled, } } func CreateTerminalIfaceModel(terminalIfaceID string, macAddress string, dhcp4 bool, dhcp6 bool, gateway4 *string, gateway6 *string, terminalID string) model.TerminalInterface { return model.TerminalInterface{ TerminalInterfaceID: terminalIfaceID, MacAddress: macAddress, Dhcp4: dhcp4, Dhcp6: dhcp6, Gateway4: gateway4, Gateway6: gateway6, TerminalID: terminalID, } } func CreateTerminalAddressModel(terminalAddressID string, ip *string, prefixLen int, family model.InetType, terminalIfaceID string) model.TerminalAddress { return model.TerminalAddress{ TerminalAddressID: terminalAddressID, IP: ip, PrefixLen: prefixLen, Family: family, TerminalInterfaceID: terminalIfaceID, } } func CreateTerminalDiskModel(terminalDiskID string, terminalID string, includeDisk bool, expectEmpty bool, devicePath string, usePart bool) model.TerminalDisk { return model.TerminalDisk{ TerminalDiskID: terminalDiskID, TerminalID: terminalID, IncludeDisk: includeDisk, ExpectEmpty: expectEmpty, DevicePath: devicePath, UsePart: usePart, } } func ValidateTerminalCreateInput(terminalInput *model.TerminalCreateInput) error { //TODO: validate terminal //TODO: validate terminal disks for _, interfaceInput := range terminalInput.Interfaces { if err := ValidateTerminalInterfaceCreateInput(interfaceInput); err != nil { return err } } return nil } func ValidateTerminalUpdateInput(terminalInput *model.TerminalUpdateInput) error { //TODO: validate terminal //TODO: validate terminal disks for _, interfaceInput := range terminalInput.Interfaces { if err := ValidateTerminalInterfaceUpdateInput(interfaceInput.TerminalInterfaceValues); err != nil { return err } } return nil } // Validate a terminal // TODO: remove when input validation is complete func ValidateTerminal(terminal *model.Terminal) error { if !terminal.Role.IsValid() { return fmt.Errorf("invalid terminal role: %s", terminal.Role.String()) } if terminal.Class != nil { if !terminal.Class.IsValid() { return fmt.Errorf("invalid terminal class: %s", terminal.Class.String()) } } if terminal.DiscoverDisks != nil { if !terminal.DiscoverDisks.IsValid() { return fmt.Errorf("invalid discoverDisks value: %s", terminal.DiscoverDisks.String()) } } return nil } func ValidateTerminalInterfaceCreateInput(interfaceInput *model.TerminalInterfaceCreateInput) error { if interfaceInput.Gateway4 != nil && !networkvalidator.ValidateIP(*interfaceInput.Gateway4) { return fmt.Errorf("invalid gateway4 address: %s", *interfaceInput.Gateway4) } if interfaceInput.Gateway6 != nil && !networkvalidator.ValidateIP(*interfaceInput.Gateway6) { return fmt.Errorf("invalid gateway6 address: %s", *interfaceInput.Gateway6) } formattedMac, err := FormatMacAddress(interfaceInput.MacAddress) if err != nil { return err } interfaceInput.MacAddress = formattedMac //TODO: validate addresses return nil } func ValidateTerminalInterfaceUpdateInput(interfaceInput *model.TerminalInterfaceUpdateInput) error { if interfaceInput.Gateway4 != nil && !networkvalidator.ValidateIP(*interfaceInput.Gateway4) { return fmt.Errorf("invalid gateway4 address: %s", *interfaceInput.Gateway4) } if interfaceInput.Gateway6 != nil && !networkvalidator.ValidateIP(*interfaceInput.Gateway6) { return fmt.Errorf("invalid gateway6 address: %s", *interfaceInput.Gateway6) } if interfaceInput.MacAddress != nil { formattedMac, err := FormatMacAddress(*interfaceInput.MacAddress) if err != nil { return err } interfaceInput.MacAddress = &formattedMac } //TODO: validate addresses return nil } // Validate a terminal interface // TODO: remove when input validation is complete func ValidateTerminalIface(iface *model.TerminalInterface) error { if iface.Gateway4 != nil && !networkvalidator.ValidateIP(*iface.Gateway4) { return fmt.Errorf("invalid gateway4 address: %s", *iface.Gateway4) } formattedMac, err := FormatMacAddress(iface.MacAddress) if err != nil { return err } iface.MacAddress = formattedMac return nil } func TerminalAddressesContainsIPv4(addrs []*model.TerminalAddress) (bool, error) { for _, addr := range addrs { if addr.Family == model.InetTypeInet && addr.IP != nil { ipAddr, err := netip.ParseAddr(*addr.IP) if err != nil { return false, err } if ipAddr.Is4() { // IPv4 address exists return true, nil } } } return false, nil } // Validate a terminal address func ValidateTerminalAddress(address *model.TerminalAddress) error { if address.IP != nil { if !networkvalidator.ValidateIP(*address.IP) { return fmt.Errorf("invalid address ip: %s", *address.IP) } if !networkvalidator.ValidateCIDR(*address.IP, address.PrefixLen) { return fmt.Errorf("invalid prefix: %d", address.PrefixLen) } } return nil } // Validate all terminal addresses func ValidateAllTerminalAddresses(dhcp4 bool, addresses []*model.TerminalAddress) error { if !dhcp4 { containsIPv4, err := TerminalAddressesContainsIPv4(addresses) if err != nil { return err } if !(containsIPv4 || dhcp4) { return errors.New("terminal address validation failed - missing ipv4 address") } } return nil } // Set the terminal values that aren't being updated to the current values in the db. func UpdateTerminal(currentTerminal *model.Terminal, updateTerminal *model.TerminalIDInput) *model.Terminal { currentTerminal.Lane = AssignNotNil(currentTerminal.Lane, updateTerminal.TerminalValues.Lane) currentTerminal.Role = *AssignNotNil(¤tTerminal.Role, updateTerminal.TerminalValues.Role) currentTerminal.Class = AssignNotNil(currentTerminal.Class, updateTerminal.TerminalValues.Class) currentTerminal.DiscoverDisks = AssignNotNil(currentTerminal.DiscoverDisks, updateTerminal.TerminalValues.DiscoverDisks) currentTerminal.BootDisk = AssignNotNil(currentTerminal.BootDisk, updateTerminal.TerminalValues.BootDisk) currentTerminal.PrimaryInterface = AssignNotNil(currentTerminal.PrimaryInterface, updateTerminal.TerminalValues.PrimaryInterface) currentTerminal.ExistingEfiPart = AssignNotNil(currentTerminal.ExistingEfiPart, updateTerminal.TerminalValues.ExistingEfiPart) currentTerminal.SwapEnabled = *AssignNotNil(¤tTerminal.SwapEnabled, updateTerminal.TerminalValues.SwapEnabled) return currentTerminal } // Updates the current terminal with new values func MergeTerminalInterface(currentIface *model.TerminalInterface, newIface *model.TerminalInterfaceUpdateInput) *model.TerminalInterface { currentIface.Dhcp4 = *AssignNotNil(¤tIface.Dhcp4, newIface.Dhcp4) currentIface.Dhcp6 = *AssignNotNil(¤tIface.Dhcp6, newIface.Dhcp6) currentIface.Gateway4 = AssignNotNil(currentIface.Gateway4, newIface.Gateway4) currentIface.Gateway6 = AssignNotNil(currentIface.Gateway6, newIface.Gateway6) currentIface.MacAddress = *AssignNotNil(¤tIface.MacAddress, newIface.MacAddress) return currentIface } // nolint // Finds matching interface and updates the current values with new values func UpdateTerminalInterface(currentTerminalInterfaces []*model.TerminalInterface, updateInterface *model.TerminalInterfaceIDInput) (*model.TerminalInterface, error) { for _, currentInterface := range currentTerminalInterfaces { if currentInterface.TerminalInterfaceID == updateInterface.TerminalInterfaceID { return MergeTerminalInterface(currentInterface, updateInterface.TerminalInterfaceValues), nil } } return nil, errors.New("cannot update a terminal interface that does not exist") } func MergeTerminalAddress(currentAddress *model.TerminalAddress, newAddress *model.TerminalAddressUpdateInput) *model.TerminalAddress { currentAddress.IP = AssignNotNil(currentAddress.IP, newAddress.IP) currentAddress.Family = *AssignNotNil(¤tAddress.Family, newAddress.Family) currentAddress.PrefixLen = *AssignNotNil(¤tAddress.PrefixLen, newAddress.PrefixLen) return currentAddress } // Set the terminal address values that aren't being updated to the current values in the db. func UpdateTerminalAddress(currentTerminalAddresses []*model.TerminalAddress, updateAddress *model.TerminalAddressIDInput) (*model.TerminalAddress, error) { for _, currentAddress := range currentTerminalAddresses { if currentAddress.TerminalAddressID == updateAddress.TerminalAddressID { return MergeTerminalAddress(currentAddress, updateAddress.TerminalAddressValues), nil } } return nil, errors.New("cannot update a terminal address that does not already exist") } func CreateIENodeHostname(macAddress string) string { normalisedMac := strings.ReplaceAll(macAddress, ":", "") return fmt.Sprintf("ien-%s", normalisedMac) } // ConvertNullIface takes an interface of null sql types and converts the object // to the TerminalInterface type defined in model. Returns an object of type // TerminalInterface and an error. func ConvertNullIface(iface NullIface) (*model.TerminalInterface, error) { return &model.TerminalInterface{ TerminalInterfaceID: iface.TerminalInterfaceID.String, TerminalID: iface.TerminalID.String, MacAddress: iface.MacAddress.String, Dhcp4: iface.Dhcp4.Bool, Dhcp6: iface.Dhcp6.Bool, Gateway4: &iface.Gateway4.String, Gateway6: &iface.Gateway6.String, }, nil } // ConvertNullAddr takes an address of null sql types and converts the object to // the TerminalAddress type defined in model. Returns an object of type // TerminalAddress and an error. func ConvertNullAddr(addr NullAddress) (*model.TerminalAddress, error) { return &model.TerminalAddress{ TerminalAddressID: addr.TerminalAddressID.String, TerminalInterfaceID: addr.TerminalInterfaceID.String, IP: &addr.IP.String, PrefixLen: int(addr.PrefixLen.Int16), Family: addr.Family.Inet, }, nil } // MatchTerminalMacAddresses compares the given MAC addresses with those registered with // the terminal in order to prevent accidental mis-registration. Returns true if there was // a match between at least one of the MAC addresses in the two lists or false if there // was no match. func MatchTerminalMacAddresses(queryMacs []string, terminal *model.Terminal) (bool, error) { if queryMacs == nil || terminal == nil { return false, nil } queryMacsSet := set.FromSlice(queryMacs) terminalMacsSet := set.Set[string]{} for _, iface := range terminal.Interfaces { convertedMac, err := FormatMacAddress(iface.MacAddress) if err != nil { return false, err } terminalMacsSet.Add(convertedMac) } return len(terminalMacsSet.Intersection(queryMacsSet)) != 0, nil } func FormatMacAddress(mac string) (string, error) { return networkvalidator.ValidateMacAddress(mac) } // FormatMacAddresses ensures that all MACs in the given list are a standard format. Returns the // converted addresses and an error. func FormatMacAddresses(macs []string) ([]string, error) { for i, mac := range macs { convertedMac, err := FormatMacAddress(mac) if err != nil { return nil, err } macs[i] = convertedMac } return macs, nil } func UpdateTerminalDisk(currentDisk *model.TerminalDisk, updateDisk *model.TerminalDiskUpdateInput) (*model.TerminalDisk, error) { currentDisk.DevicePath = *AssignNotNil(¤tDisk.DevicePath, updateDisk.DevicePath) currentDisk.ExpectEmpty = *AssignNotNil(¤tDisk.ExpectEmpty, updateDisk.ExpectEmpty) currentDisk.IncludeDisk = *AssignNotNil(¤tDisk.IncludeDisk, updateDisk.IncludeDisk) currentDisk.UsePart = *AssignNotNil(¤tDisk.UsePart, updateDisk.UsePart) return currentDisk, nil }