...

Source file src/edge-infra.dev/pkg/edge/api/utils/terminal_helper.go

Documentation: edge-infra.dev/pkg/edge/api/utils

     1  package utils
     2  
     3  import (
     4  	"database/sql"
     5  	"database/sql/driver"
     6  	"errors"
     7  	"fmt"
     8  	"net/netip"
     9  	"strings"
    10  
    11  	"edge-infra.dev/pkg/edge/api/graph/model"
    12  	"edge-infra.dev/pkg/lib/networkvalidator"
    13  	"edge-infra.dev/pkg/sds/lib/set"
    14  )
    15  
    16  type NullInet struct {
    17  	Inet  model.InetType
    18  	Valid bool // Valid is true if Inet is not NULL
    19  }
    20  
    21  // Scan implements the Scanner interface.
    22  func (ni *NullInet) Scan(value any) error {
    23  	if value == nil {
    24  		ni.Valid = false
    25  		return nil
    26  	}
    27  	ni.Valid = true
    28  	return ni.Inet.UnmarshalGQL(value)
    29  }
    30  
    31  // Value implements the driver Valuer interface.
    32  func (ni NullInet) Value() (driver.Value, error) {
    33  	if !ni.Valid {
    34  		return nil, nil
    35  	}
    36  	return ni.Inet, nil
    37  }
    38  
    39  type NullIface struct {
    40  	TerminalInterfaceID sql.NullString
    41  	MacAddress          sql.NullString
    42  	Dhcp4               sql.NullBool
    43  	Dhcp6               sql.NullBool
    44  	Gateway4            sql.NullString
    45  	Gateway6            sql.NullString
    46  	TerminalID          sql.NullString
    47  }
    48  type NullAddress struct {
    49  	TerminalAddressID   sql.NullString
    50  	IP                  sql.NullString
    51  	PrefixLen           sql.NullInt16
    52  	Family              NullInet
    53  	TerminalInterfaceID sql.NullString
    54  }
    55  
    56  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 {
    57  	discoverDisks := model.TerminalDiscoverDisksTypeEmpty
    58  	if terminalDiscoverDisks == nil {
    59  		terminalDiscoverDisks = &discoverDisks
    60  	}
    61  	return model.Terminal{
    62  		TerminalID:      terminalID,
    63  		Lane:            lane,
    64  		Role:            terminalRole,
    65  		Class:           terminalClass,
    66  		DiscoverDisks:   terminalDiscoverDisks,
    67  		BootDisk:        terminalBootDisk,
    68  		ClusterEdgeID:   clusterEdgeID,
    69  		ClusterName:     clusterName,
    70  		Hostname:        hostname,
    71  		ExistingEfiPart: existingEfiPart,
    72  		SwapEnabled:     swapEnabled,
    73  	}
    74  }
    75  
    76  func CreateTerminalIfaceModel(terminalIfaceID string, macAddress string, dhcp4 bool, dhcp6 bool, gateway4 *string, gateway6 *string, terminalID string) model.TerminalInterface {
    77  	return model.TerminalInterface{
    78  		TerminalInterfaceID: terminalIfaceID,
    79  		MacAddress:          macAddress,
    80  		Dhcp4:               dhcp4,
    81  		Dhcp6:               dhcp6,
    82  		Gateway4:            gateway4,
    83  		Gateway6:            gateway6,
    84  		TerminalID:          terminalID,
    85  	}
    86  }
    87  
    88  func CreateTerminalAddressModel(terminalAddressID string, ip *string, prefixLen int, family model.InetType, terminalIfaceID string) model.TerminalAddress {
    89  	return model.TerminalAddress{
    90  		TerminalAddressID:   terminalAddressID,
    91  		IP:                  ip,
    92  		PrefixLen:           prefixLen,
    93  		Family:              family,
    94  		TerminalInterfaceID: terminalIfaceID,
    95  	}
    96  }
    97  
    98  func CreateTerminalDiskModel(terminalDiskID string, terminalID string, includeDisk bool, expectEmpty bool, devicePath string, usePart bool) model.TerminalDisk {
    99  	return model.TerminalDisk{
   100  		TerminalDiskID: terminalDiskID,
   101  		TerminalID:     terminalID,
   102  		IncludeDisk:    includeDisk,
   103  		ExpectEmpty:    expectEmpty,
   104  		DevicePath:     devicePath,
   105  		UsePart:        usePart,
   106  	}
   107  }
   108  
   109  func ValidateTerminalCreateInput(terminalInput *model.TerminalCreateInput) error {
   110  	//TODO: validate terminal
   111  
   112  	//TODO: validate terminal disks
   113  
   114  	for _, interfaceInput := range terminalInput.Interfaces {
   115  		if err := ValidateTerminalInterfaceCreateInput(interfaceInput); err != nil {
   116  			return err
   117  		}
   118  	}
   119  	return nil
   120  }
   121  
   122  func ValidateTerminalUpdateInput(terminalInput *model.TerminalUpdateInput) error {
   123  	//TODO: validate terminal
   124  
   125  	//TODO: validate terminal disks
   126  
   127  	for _, interfaceInput := range terminalInput.Interfaces {
   128  		if err := ValidateTerminalInterfaceUpdateInput(interfaceInput.TerminalInterfaceValues); err != nil {
   129  			return err
   130  		}
   131  	}
   132  	return nil
   133  }
   134  
   135  // Validate a terminal
   136  // TODO: remove when input validation is complete
   137  func ValidateTerminal(terminal *model.Terminal) error {
   138  	if !terminal.Role.IsValid() {
   139  		return fmt.Errorf("invalid terminal role: %s", terminal.Role.String())
   140  	}
   141  
   142  	if terminal.Class != nil {
   143  		if !terminal.Class.IsValid() {
   144  			return fmt.Errorf("invalid terminal class: %s", terminal.Class.String())
   145  		}
   146  	}
   147  
   148  	if terminal.DiscoverDisks != nil {
   149  		if !terminal.DiscoverDisks.IsValid() {
   150  			return fmt.Errorf("invalid discoverDisks value: %s", terminal.DiscoverDisks.String())
   151  		}
   152  	}
   153  
   154  	return nil
   155  }
   156  
   157  func ValidateTerminalInterfaceCreateInput(interfaceInput *model.TerminalInterfaceCreateInput) error {
   158  	if interfaceInput.Gateway4 != nil && !networkvalidator.ValidateIP(*interfaceInput.Gateway4) {
   159  		return fmt.Errorf("invalid gateway4 address: %s", *interfaceInput.Gateway4)
   160  	}
   161  	if interfaceInput.Gateway6 != nil && !networkvalidator.ValidateIP(*interfaceInput.Gateway6) {
   162  		return fmt.Errorf("invalid gateway6 address: %s", *interfaceInput.Gateway6)
   163  	}
   164  
   165  	formattedMac, err := FormatMacAddress(interfaceInput.MacAddress)
   166  	if err != nil {
   167  		return err
   168  	}
   169  	interfaceInput.MacAddress = formattedMac
   170  
   171  	//TODO: validate addresses
   172  
   173  	return nil
   174  }
   175  
   176  func ValidateTerminalInterfaceUpdateInput(interfaceInput *model.TerminalInterfaceUpdateInput) error {
   177  	if interfaceInput.Gateway4 != nil && !networkvalidator.ValidateIP(*interfaceInput.Gateway4) {
   178  		return fmt.Errorf("invalid gateway4 address: %s", *interfaceInput.Gateway4)
   179  	}
   180  	if interfaceInput.Gateway6 != nil && !networkvalidator.ValidateIP(*interfaceInput.Gateway6) {
   181  		return fmt.Errorf("invalid gateway6 address: %s", *interfaceInput.Gateway6)
   182  	}
   183  
   184  	if interfaceInput.MacAddress != nil {
   185  		formattedMac, err := FormatMacAddress(*interfaceInput.MacAddress)
   186  		if err != nil {
   187  			return err
   188  		}
   189  		interfaceInput.MacAddress = &formattedMac
   190  	}
   191  
   192  	//TODO: validate addresses
   193  
   194  	return nil
   195  }
   196  
   197  // Validate a terminal interface
   198  // TODO: remove when input validation is complete
   199  func ValidateTerminalIface(iface *model.TerminalInterface) error {
   200  	if iface.Gateway4 != nil && !networkvalidator.ValidateIP(*iface.Gateway4) {
   201  		return fmt.Errorf("invalid gateway4 address: %s", *iface.Gateway4)
   202  	}
   203  
   204  	formattedMac, err := FormatMacAddress(iface.MacAddress)
   205  	if err != nil {
   206  		return err
   207  	}
   208  	iface.MacAddress = formattedMac
   209  	return nil
   210  }
   211  
   212  func TerminalAddressesContainsIPv4(addrs []*model.TerminalAddress) (bool, error) {
   213  	for _, addr := range addrs {
   214  		if addr.Family == model.InetTypeInet && addr.IP != nil {
   215  			ipAddr, err := netip.ParseAddr(*addr.IP)
   216  			if err != nil {
   217  				return false, err
   218  			}
   219  			if ipAddr.Is4() {
   220  				// IPv4 address exists
   221  				return true, nil
   222  			}
   223  		}
   224  	}
   225  	return false, nil
   226  }
   227  
   228  // Validate a terminal address
   229  func ValidateTerminalAddress(address *model.TerminalAddress) error {
   230  	if address.IP != nil {
   231  		if !networkvalidator.ValidateIP(*address.IP) {
   232  			return fmt.Errorf("invalid address ip: %s", *address.IP)
   233  		}
   234  		if !networkvalidator.ValidateCIDR(*address.IP, address.PrefixLen) {
   235  			return fmt.Errorf("invalid prefix: %d", address.PrefixLen)
   236  		}
   237  	}
   238  	return nil
   239  }
   240  
   241  // Validate all terminal addresses
   242  func ValidateAllTerminalAddresses(dhcp4 bool, addresses []*model.TerminalAddress) error {
   243  	if !dhcp4 {
   244  		containsIPv4, err := TerminalAddressesContainsIPv4(addresses)
   245  		if err != nil {
   246  			return err
   247  		}
   248  		if !(containsIPv4 || dhcp4) {
   249  			return errors.New("terminal address validation failed - missing ipv4 address")
   250  		}
   251  	}
   252  	return nil
   253  }
   254  
   255  // Set the terminal values that aren't being updated to the current values in the db.
   256  func UpdateTerminal(currentTerminal *model.Terminal, updateTerminal *model.TerminalIDInput) *model.Terminal {
   257  	currentTerminal.Lane = AssignNotNil(currentTerminal.Lane, updateTerminal.TerminalValues.Lane)
   258  	currentTerminal.Role = *AssignNotNil(&currentTerminal.Role, updateTerminal.TerminalValues.Role)
   259  	currentTerminal.Class = AssignNotNil(currentTerminal.Class, updateTerminal.TerminalValues.Class)
   260  	currentTerminal.DiscoverDisks = AssignNotNil(currentTerminal.DiscoverDisks, updateTerminal.TerminalValues.DiscoverDisks)
   261  	currentTerminal.BootDisk = AssignNotNil(currentTerminal.BootDisk, updateTerminal.TerminalValues.BootDisk)
   262  	currentTerminal.PrimaryInterface = AssignNotNil(currentTerminal.PrimaryInterface, updateTerminal.TerminalValues.PrimaryInterface)
   263  	currentTerminal.ExistingEfiPart = AssignNotNil(currentTerminal.ExistingEfiPart, updateTerminal.TerminalValues.ExistingEfiPart)
   264  	currentTerminal.SwapEnabled = *AssignNotNil(&currentTerminal.SwapEnabled, updateTerminal.TerminalValues.SwapEnabled)
   265  	return currentTerminal
   266  }
   267  
   268  // Updates the current terminal with new values
   269  func MergeTerminalInterface(currentIface *model.TerminalInterface, newIface *model.TerminalInterfaceUpdateInput) *model.TerminalInterface {
   270  	currentIface.Dhcp4 = *AssignNotNil(&currentIface.Dhcp4, newIface.Dhcp4)
   271  	currentIface.Dhcp6 = *AssignNotNil(&currentIface.Dhcp6, newIface.Dhcp6)
   272  	currentIface.Gateway4 = AssignNotNil(currentIface.Gateway4, newIface.Gateway4)
   273  	currentIface.Gateway6 = AssignNotNil(currentIface.Gateway6, newIface.Gateway6)
   274  	currentIface.MacAddress = *AssignNotNil(&currentIface.MacAddress, newIface.MacAddress)
   275  	return currentIface
   276  }
   277  
   278  // nolint
   279  // Finds matching interface and updates the current values with new values
   280  func UpdateTerminalInterface(currentTerminalInterfaces []*model.TerminalInterface, updateInterface *model.TerminalInterfaceIDInput) (*model.TerminalInterface, error) {
   281  	for _, currentInterface := range currentTerminalInterfaces {
   282  		if currentInterface.TerminalInterfaceID == updateInterface.TerminalInterfaceID {
   283  			return MergeTerminalInterface(currentInterface, updateInterface.TerminalInterfaceValues), nil
   284  		}
   285  	}
   286  	return nil, errors.New("cannot update a terminal interface that does not exist")
   287  }
   288  
   289  func MergeTerminalAddress(currentAddress *model.TerminalAddress, newAddress *model.TerminalAddressUpdateInput) *model.TerminalAddress {
   290  	currentAddress.IP = AssignNotNil(currentAddress.IP, newAddress.IP)
   291  	currentAddress.Family = *AssignNotNil(&currentAddress.Family, newAddress.Family)
   292  	currentAddress.PrefixLen = *AssignNotNil(&currentAddress.PrefixLen, newAddress.PrefixLen)
   293  	return currentAddress
   294  }
   295  
   296  // Set the terminal address values that aren't being updated to the current values in the db.
   297  func UpdateTerminalAddress(currentTerminalAddresses []*model.TerminalAddress, updateAddress *model.TerminalAddressIDInput) (*model.TerminalAddress, error) {
   298  	for _, currentAddress := range currentTerminalAddresses {
   299  		if currentAddress.TerminalAddressID == updateAddress.TerminalAddressID {
   300  			return MergeTerminalAddress(currentAddress, updateAddress.TerminalAddressValues), nil
   301  		}
   302  	}
   303  	return nil, errors.New("cannot update a terminal address that does not already exist")
   304  }
   305  
   306  func CreateIENodeHostname(macAddress string) string {
   307  	normalisedMac := strings.ReplaceAll(macAddress, ":", "")
   308  	return fmt.Sprintf("ien-%s", normalisedMac)
   309  }
   310  
   311  // ConvertNullIface takes an interface of null sql types and converts the object
   312  // to the TerminalInterface type defined in model. Returns an object of type
   313  // TerminalInterface and an error.
   314  func ConvertNullIface(iface NullIface) (*model.TerminalInterface, error) {
   315  	return &model.TerminalInterface{
   316  		TerminalInterfaceID: iface.TerminalInterfaceID.String,
   317  		TerminalID:          iface.TerminalID.String,
   318  		MacAddress:          iface.MacAddress.String,
   319  		Dhcp4:               iface.Dhcp4.Bool,
   320  		Dhcp6:               iface.Dhcp6.Bool,
   321  		Gateway4:            &iface.Gateway4.String,
   322  		Gateway6:            &iface.Gateway6.String,
   323  	}, nil
   324  }
   325  
   326  // ConvertNullAddr takes an address of null sql types and converts the object to
   327  // the TerminalAddress type defined in model. Returns an object of type
   328  // TerminalAddress and an error.
   329  func ConvertNullAddr(addr NullAddress) (*model.TerminalAddress, error) {
   330  	return &model.TerminalAddress{
   331  		TerminalAddressID:   addr.TerminalAddressID.String,
   332  		TerminalInterfaceID: addr.TerminalInterfaceID.String,
   333  		IP:                  &addr.IP.String,
   334  		PrefixLen:           int(addr.PrefixLen.Int16),
   335  		Family:              addr.Family.Inet,
   336  	}, nil
   337  }
   338  
   339  // MatchTerminalMacAddresses compares the given MAC addresses with those registered with
   340  // the terminal in order to prevent accidental mis-registration. Returns true if there was
   341  // a match between at least one of the MAC addresses in the two lists or false if there
   342  // was no match.
   343  func MatchTerminalMacAddresses(queryMacs []string, terminal *model.Terminal) (bool, error) {
   344  	if queryMacs == nil || terminal == nil {
   345  		return false, nil
   346  	}
   347  
   348  	queryMacsSet := set.FromSlice(queryMacs)
   349  	terminalMacsSet := set.Set[string]{}
   350  	for _, iface := range terminal.Interfaces {
   351  		convertedMac, err := FormatMacAddress(iface.MacAddress)
   352  		if err != nil {
   353  			return false, err
   354  		}
   355  		terminalMacsSet.Add(convertedMac)
   356  	}
   357  
   358  	return len(terminalMacsSet.Intersection(queryMacsSet)) != 0, nil
   359  }
   360  
   361  func FormatMacAddress(mac string) (string, error) {
   362  	return networkvalidator.ValidateMacAddress(mac)
   363  }
   364  
   365  // FormatMacAddresses ensures that all MACs in the given list are a standard format. Returns the
   366  // converted addresses and an error.
   367  func FormatMacAddresses(macs []string) ([]string, error) {
   368  	for i, mac := range macs {
   369  		convertedMac, err := FormatMacAddress(mac)
   370  		if err != nil {
   371  			return nil, err
   372  		}
   373  		macs[i] = convertedMac
   374  	}
   375  	return macs, nil
   376  }
   377  
   378  func UpdateTerminalDisk(currentDisk *model.TerminalDisk, updateDisk *model.TerminalDiskUpdateInput) (*model.TerminalDisk, error) {
   379  	currentDisk.DevicePath = *AssignNotNil(&currentDisk.DevicePath, updateDisk.DevicePath)
   380  	currentDisk.ExpectEmpty = *AssignNotNil(&currentDisk.ExpectEmpty, updateDisk.ExpectEmpty)
   381  	currentDisk.IncludeDisk = *AssignNotNil(&currentDisk.IncludeDisk, updateDisk.IncludeDisk)
   382  	currentDisk.UsePart = *AssignNotNil(&currentDisk.UsePart, updateDisk.UsePart)
   383  
   384  	return currentDisk, nil
   385  }
   386  

View as plain text