...

Source file src/edge-infra.dev/pkg/edge/api/services/terminal_service.go

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

     1  package services
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"slices"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/google/uuid"
    15  	"gopkg.in/yaml.v2"
    16  
    17  	sqlerr "edge-infra.dev/pkg/edge/api/apierror/sql"
    18  	"edge-infra.dev/pkg/edge/api/clients"
    19  	"edge-infra.dev/pkg/edge/api/graph/mapper"
    20  	"edge-infra.dev/pkg/edge/api/graph/model"
    21  	edgenode "edge-infra.dev/pkg/edge/api/services/edgenode/common"
    22  	sqlquery "edge-infra.dev/pkg/edge/api/sql"
    23  	"edge-infra.dev/pkg/edge/api/status"
    24  	"edge-infra.dev/pkg/edge/api/utils"
    25  	"edge-infra.dev/pkg/lib/crypto"
    26  	"edge-infra.dev/pkg/lib/runtime/version"
    27  	cc "edge-infra.dev/pkg/sds/clustersecrets/common"
    28  	dsv1 "edge-infra.dev/pkg/sds/devices/k8s/apis/v1"
    29  	v1ien "edge-infra.dev/pkg/sds/ien/k8s/apis/v1"
    30  )
    31  
    32  //go:generate mockgen -destination=../mocks/mock_terminal_service.go -package=mocks edge-infra.dev/pkg/edge/api/services TerminalService
    33  type TerminalService interface {
    34  	CreateTerminalEntry(ctx context.Context, newTerminal *model.TerminalCreateInput, activationCode crypto.Credential) (*model.Terminal, error)
    35  	UpdateTerminalEntry(ctx context.Context, terminal *model.TerminalIDInput) (*model.Terminal, error)
    36  	DeleteTerminalEntry(ctx context.Context, terminalID string) error
    37  	CreateTerminalInterfaceEntry(ctx context.Context, terminalID string, newTerminalInterface *model.TerminalInterfaceCreateInput) (*model.TerminalInterface, error)
    38  	UpdateTerminalInterfaceEntry(ctx context.Context, updatedInterface *model.TerminalInterfaceIDInput) (*model.TerminalInterface, error)
    39  	DeleteTerminalInterfaceEntry(ctx context.Context, terminalInterfaceID string) (*model.Terminal, error)
    40  	CreateTerminalAddressEntry(ctx context.Context, terminalInterfaceID string, newTerminalAddress *model.TerminalAddressCreateInput) (*model.TerminalAddress, error)
    41  	UpdateTerminalAddressEntry(ctx context.Context, updatedAddress *model.TerminalAddressIDInput) (*model.TerminalAddress, error)
    42  	DeleteTerminalAddressEntry(ctx context.Context, terminalAddressID string) (*model.Terminal, error)
    43  	GetTerminal(ctx context.Context, terminalID string, getLabels *bool) (*model.Terminal, error)
    44  	GetTerminals(ctx context.Context, clusterEdgeID *string, terminalHostname *string) ([]*model.Terminal, error)
    45  	GetBannerTerminals(ctx context.Context, bannerEdgeID, projectID string) ([]*model.Terminal, error)
    46  	CreateDSDSIENodeCR(terminal *model.Terminal, clusterNetworkServices []*model.ClusterNetworkServiceInfo, customLabels map[string]string, edgeVersion string) (string, error)
    47  	CreateCICIENodeCR(terminal *model.Terminal, clusterNetworkServices []*model.ClusterNetworkServiceInfo, customLabels map[string]string, edgeVersion string) (string, error)
    48  	GetTerminalFromInterface(ctx context.Context, terminalInterfaceID string) (*model.Terminal, error)
    49  	GetTerminalFromAddress(ctx context.Context, terminalAddressID string) (*model.Terminal, error)
    50  	GetTerminalBootstrapConfig(ctx context.Context, terminal *model.Terminal, clusterNetworkServices []*model.ClusterNetworkServiceInfo, breakGlassSecret, grubSecret cc.Secret, bootstrapAck, isFirstNode bool, organization string, bootstrapTokenValues []*model.KeyValues, clusterCaHash, endpoint string) (string, error)
    51  	RemoveClusterEdgeBootstrapToken(ctx context.Context, clusterEdgeID string) error
    52  	CreateTerminalDiskEntry(ctx context.Context, terminalID string, newTerminalDisk *model.TerminalDiskCreateInput) (*model.TerminalDisk, error)
    53  	DeleteTerminalDiskEntry(ctx context.Context, terminalDiskID string) (*model.Terminal, error)
    54  	UpdateTerminalDiskEntry(ctx context.Context, terminalDiskID string, updatedDisk model.TerminalDiskUpdateInput) (*model.TerminalDisk, error)
    55  	GetTerminalFromDisk(ctx context.Context, terminalDiskID string) (*model.Terminal, error)
    56  	GetTerminalNodeStatus(ctx context.Context, clusterEdgeID, terminalID, hostname string) (*model.TerminalStatus, error)
    57  	GetNodeReplicationStatus(ctx context.Context, clusterEdgeID, hostname string) (*model.ReplicationStatus, error)
    58  	GetTerminalsByClusterID(ctx context.Context, clusterEdgeID string) ([]*model.Terminal, error)
    59  	TerminalDevices(ctx context.Context, terminalID string) (*model.TerminalDevices, error)
    60  }
    61  
    62  type terminalService struct {
    63  	SQLDB               *sql.DB
    64  	BQClient            clients.BQClient
    65  	LabelService        LabelService
    66  	StoreClusterService StoreClusterService
    67  }
    68  
    69  var (
    70  	getLabel                            = true
    71  	ErrDuplicateTerminalDiskDevicePaths = errors.New("terminals cannot have disks with duplicate device paths")
    72  )
    73  
    74  func (t *terminalService) CreateTerminalEntry(ctx context.Context, newTerminal *model.TerminalCreateInput, activationCode crypto.Credential) (*model.Terminal, error) {
    75  	if err := utils.ValidateTerminalCreateInput(newTerminal); err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	transaction, err := t.SQLDB.BeginTx(ctx, nil)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	defer transaction.Rollback() //nolint
    84  
    85  	row := transaction.QueryRowContext(ctx, sqlquery.GetClusterNameByClusterEdgeIDQuery, newTerminal.ClusterEdgeID)
    86  	var clusterName string
    87  	err = row.Scan(&clusterName)
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  
    92  	// create terminal
    93  	if len(newTerminal.Interfaces) == 0 {
    94  		return nil, errors.New("unable to create terminal with no interfaces")
    95  	}
    96  
    97  	var hostname string
    98  	if newTerminal.Hostname == nil {
    99  		hostname = utils.CreateIENodeHostname(newTerminal.Interfaces[0].MacAddress)
   100  	} else {
   101  		hostname = strings.ToLower(*newTerminal.Hostname)
   102  	}
   103  	newTerminal.Hostname = &hostname
   104  
   105  	// check existing hostnames
   106  	if err := t.checkHostnames(ctx, hostname, newTerminal); err != nil {
   107  		return nil, err
   108  	}
   109  
   110  	// check lane value is unique
   111  	if newTerminal.Lane != nil && *newTerminal.Lane != "" {
   112  		if err := t.checkLaneDuplicates(ctx, *newTerminal.Lane, newTerminal.ClusterEdgeID); err != nil {
   113  			return nil, err
   114  		}
   115  	}
   116  
   117  	terminal := utils.CreateTerminalModel(uuid.NewString(), newTerminal.ClusterEdgeID, newTerminal.Lane, newTerminal.Role, newTerminal.Class, newTerminal.DiscoverDisks, newTerminal.BootDisk, clusterName, *newTerminal.Hostname, newTerminal.ExistingEfiPart, *newTerminal.SwapEnabled)
   118  
   119  	// validate terminal
   120  	if err = utils.ValidateTerminal(&terminal); err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	hash := activationCode.Hashed()
   125  	hashEncoded := hex.EncodeToString(hash)
   126  
   127  	args := []interface{}{
   128  		terminal.TerminalID,
   129  		terminal.Lane,
   130  		terminal.Role,
   131  		terminal.Class,
   132  		terminal.DiscoverDisks,
   133  		terminal.BootDisk,
   134  		terminal.ClusterEdgeID,
   135  		terminal.Hostname,
   136  		hashEncoded,
   137  		terminal.ExistingEfiPart,
   138  		terminal.SwapEnabled,
   139  	}
   140  
   141  	if _, err = transaction.ExecContext(ctx, sqlquery.TerminalCreateQuery, args...); err != nil {
   142  		return nil, err
   143  	}
   144  
   145  	// check for duplicate device paths
   146  	if hasDuplicateDevicePathsCreate(newTerminal.Disks) {
   147  		return nil, ErrDuplicateTerminalDiskDevicePaths
   148  	}
   149  
   150  	terminalDisk, err := t.createTerminalDiskEntries(ctx, transaction, newTerminal.Disks, terminal.TerminalID)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  	terminal.Disks = terminalDisk
   155  
   156  	// create interfaces
   157  	terminal.Interfaces, err = t.createTerminalInterfaceEntries(ctx, transaction, newTerminal.Interfaces, terminal.TerminalID)
   158  	if err != nil {
   159  		if rollbackErr := transaction.Rollback(); rollbackErr != nil {
   160  			return nil, rollbackErr
   161  		}
   162  		return nil, err
   163  	}
   164  
   165  	if err = transaction.Commit(); err != nil {
   166  		return nil, err
   167  	}
   168  
   169  	return &terminal, nil
   170  }
   171  
   172  // hasDuplicateDevicePathsCreate returns true if the provided diskList contains any disks to be created with the same device path
   173  func hasDuplicateDevicePathsCreate(diskList []*model.TerminalDiskCreateInput) bool {
   174  	diskPaths := []string{}
   175  	for _, disk := range diskList {
   176  		if slices.Contains(diskPaths, disk.DevicePath) {
   177  			return true
   178  		}
   179  		diskPaths = append(diskPaths, disk.DevicePath)
   180  	}
   181  	return false
   182  }
   183  
   184  func (t *terminalService) createTerminalInterfaceEntries(ctx context.Context, transaction *sql.Tx, newInterfaces []*model.TerminalInterfaceCreateInput, terminalID string) ([]*model.TerminalInterface, error) {
   185  	terminalInterfaces := []*model.TerminalInterface{}
   186  	for _, newInterface := range newInterfaces {
   187  		terminalInterface := utils.CreateTerminalIfaceModel(uuid.NewString(), strings.ToLower(newInterface.MacAddress), newInterface.Dhcp4, newInterface.Dhcp6, newInterface.Gateway4, newInterface.Gateway6, terminalID)
   188  
   189  		args := []interface{}{
   190  			terminalInterface.TerminalInterfaceID,
   191  			terminalInterface.MacAddress,
   192  			terminalInterface.Dhcp4,
   193  			terminalInterface.Dhcp6,
   194  			terminalInterface.Gateway4,
   195  			terminalInterface.Gateway6,
   196  			terminalID,
   197  		}
   198  
   199  		if _, err := transaction.ExecContext(ctx, sqlquery.TerminalInterfaceCreateQuery, args...); err != nil {
   200  			return nil, err
   201  		}
   202  
   203  		// create addresses
   204  		var err error
   205  		terminalInterface.Addresses, err = t.createTerminalAddressEntries(ctx, transaction, newInterface.Addresses, &terminalInterface)
   206  		if err != nil {
   207  			return nil, err
   208  		}
   209  		if err := utils.ValidateAllTerminalAddresses(terminalInterface.Dhcp4, terminalInterface.Addresses); err != nil {
   210  			return nil, err
   211  		}
   212  		terminalInterfaces = append(terminalInterfaces, &terminalInterface)
   213  	}
   214  	return terminalInterfaces, nil
   215  }
   216  
   217  func (t *terminalService) createTerminalAddressEntries(ctx context.Context, transaction *sql.Tx, newAddresses []*model.TerminalAddressCreateInput, iface *model.TerminalInterface) ([]*model.TerminalAddress, error) {
   218  	terminalAddresses := []*model.TerminalAddress{}
   219  	for _, newAddress := range newAddresses {
   220  		terminalAddress := utils.CreateTerminalAddressModel(uuid.NewString(), newAddress.IP, newAddress.PrefixLen, newAddress.Family, iface.TerminalInterfaceID)
   221  		// validate terminal address
   222  		if err := utils.ValidateTerminalAddress(&terminalAddress); err != nil {
   223  			return nil, err
   224  		}
   225  		args := []interface{}{
   226  			terminalAddress.TerminalAddressID,
   227  			terminalAddress.IP,
   228  			terminalAddress.PrefixLen,
   229  			terminalAddress.Family,
   230  			iface.TerminalInterfaceID,
   231  		}
   232  
   233  		if _, err := transaction.ExecContext(ctx, sqlquery.TerminalAddressCreateQuery, args...); err != nil {
   234  			return nil, err
   235  		}
   236  		terminalAddresses = append(terminalAddresses, &terminalAddress)
   237  	}
   238  	return terminalAddresses, nil
   239  }
   240  
   241  func (t *terminalService) UpdateTerminalEntry(ctx context.Context, updateTerminal *model.TerminalIDInput) (*model.Terminal, error) {
   242  	if err := utils.ValidateTerminalUpdateInput(updateTerminal.TerminalValues); err != nil {
   243  		return nil, err
   244  	}
   245  
   246  	transaction, err := t.SQLDB.BeginTx(ctx, nil)
   247  	if err != nil {
   248  		return nil, err
   249  	}
   250  	defer transaction.Rollback() //nolint
   251  
   252  	// get the current values for the terminal in the db
   253  	currentTerminal, err := t.GetTerminal(ctx, updateTerminal.TerminalID, &getLabel)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  
   258  	// check lane value is unique
   259  	if updateTerminal.TerminalValues.Lane != nil && *updateTerminal.TerminalValues.Lane != "" {
   260  		if err := t.checkLaneDuplicates(ctx, *updateTerminal.TerminalValues.Lane, currentTerminal.ClusterEdgeID); err != nil {
   261  			return nil, err
   262  		}
   263  	}
   264  
   265  	// check activation code is available before setting primary interface
   266  	if updateTerminal.TerminalValues.PrimaryInterface != nil {
   267  		ac, err := edgenode.FetchActivationCode(ctx, t.SQLDB, updateTerminal.TerminalID)
   268  		if err != nil {
   269  			return nil, err
   270  		}
   271  		if ac == nil || len(*ac) == 0 {
   272  			return nil, errors.New("cannot change primary interface, terminal already bootstrapped")
   273  		}
   274  	}
   275  
   276  	// set current terminal values
   277  	currentTerminal = utils.UpdateTerminal(currentTerminal, updateTerminal)
   278  	// validate the current terminal
   279  	if err = utils.ValidateTerminal(currentTerminal); err != nil {
   280  		return nil, err
   281  	}
   282  
   283  	args := []interface{}{
   284  		currentTerminal.Lane,
   285  		currentTerminal.Role,
   286  		currentTerminal.Class,
   287  		currentTerminal.DiscoverDisks,
   288  		currentTerminal.BootDisk,
   289  		currentTerminal.PrimaryInterface,
   290  		currentTerminal.ExistingEfiPart,
   291  		currentTerminal.SwapEnabled,
   292  		currentTerminal.TerminalID,
   293  	}
   294  
   295  	// update terminal in db
   296  	if _, err = transaction.ExecContext(ctx, sqlquery.TerminalUpdateQuery, args...); err != nil {
   297  		return nil, err
   298  	}
   299  
   300  	// check for duplicate device paths in provided update disks
   301  	if hasDuplicateDevicePathsUpdate(updateTerminal.TerminalValues.Disks) {
   302  		return nil, ErrDuplicateTerminalDiskDevicePaths
   303  	}
   304  	terminalDisks, err := t.updateTerminalDiskEntries(ctx, transaction, updateTerminal.TerminalValues.Disks)
   305  	if err != nil {
   306  		if strings.Contains(err.Error(), "duplicate key value violates unique constraint \"unique_terminal_id_device_path\"") {
   307  			return nil, ErrDuplicateTerminalDiskDevicePaths // return neat error
   308  		}
   309  		return nil, err
   310  	}
   311  	currentTerminal.Disks = terminalDisks
   312  
   313  	// update interfaces
   314  	if err := t.updateTerminalInterfaceEntries(ctx, transaction, updateTerminal.TerminalValues.Interfaces, currentTerminal.Interfaces); err != nil {
   315  		if rollbackErr := transaction.Rollback(); rollbackErr != nil {
   316  			return nil, rollbackErr
   317  		}
   318  		return nil, err
   319  	}
   320  
   321  	if err = transaction.Commit(); err != nil {
   322  		return nil, err
   323  	}
   324  
   325  	currentTerminal, err = t.GetTerminal(ctx, updateTerminal.TerminalID, &getLabel)
   326  	if err != nil {
   327  		return nil, err
   328  	}
   329  	return currentTerminal, nil
   330  }
   331  
   332  // hasDuplicateDevicePathsUpdate returns true if the provided diskList contains any disks to be updated with the same device path
   333  // only checks provided update disks, not existing disks - that is handled by DB
   334  func hasDuplicateDevicePathsUpdate(diskList []*model.TerminalDiskIDInput) bool {
   335  	diskPaths := []string{}
   336  	for _, disk := range diskList {
   337  		if disk == nil || disk.TerminalDiskValues == nil || disk.TerminalDiskValues.DevicePath == nil {
   338  			continue
   339  		}
   340  		diskPath := *disk.TerminalDiskValues.DevicePath
   341  		if slices.Contains(diskPaths, diskPath) {
   342  			return true
   343  		}
   344  		diskPaths = append(diskPaths, diskPath)
   345  	}
   346  	return false
   347  }
   348  
   349  func (t *terminalService) UpdateTerminalInterfaceEntry(ctx context.Context, updatedInterface *model.TerminalInterfaceIDInput) (*model.TerminalInterface, error) { //nolint
   350  	if err := utils.ValidateTerminalInterfaceUpdateInput(updatedInterface.TerminalInterfaceValues); err != nil {
   351  		return nil, err
   352  	}
   353  
   354  	updateIfaces := []*model.TerminalInterfaceIDInput{updatedInterface}
   355  	currentIface, err := t.getTerminalInterface(ctx, updatedInterface.TerminalInterfaceID)
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  	currentIfaces := []*model.TerminalInterface{currentIface}
   360  
   361  	transaction, err := t.SQLDB.BeginTx(ctx, nil)
   362  	if err != nil {
   363  		return nil, err
   364  	}
   365  
   366  	if err := t.updateTerminalInterfaceEntries(ctx, transaction, updateIfaces, currentIfaces); err != nil {
   367  		if rollbackErr := transaction.Rollback(); rollbackErr != nil {
   368  			return nil, rollbackErr
   369  		}
   370  		return nil, err
   371  	}
   372  
   373  	if err := transaction.Commit(); err != nil {
   374  		return nil, err
   375  	}
   376  
   377  	iface, err := t.getTerminalInterface(ctx, updatedInterface.TerminalInterfaceID)
   378  	if err != nil {
   379  		return nil, err
   380  	}
   381  	return iface, nil
   382  }
   383  
   384  func (t *terminalService) updateTerminalInterfaceEntries(ctx context.Context, transaction *sql.Tx, updateInterfaces []*model.TerminalInterfaceIDInput, currentInterfaces []*model.TerminalInterface) error {
   385  	for _, updateInterface := range updateInterfaces {
   386  		interfaceValues := updateInterface.TerminalInterfaceValues
   387  
   388  		// update the current interface's values
   389  		currentInterface, err := utils.UpdateTerminalInterface(currentInterfaces, updateInterface)
   390  		if err != nil {
   391  			return err
   392  		}
   393  
   394  		args := []interface{}{
   395  			currentInterface.MacAddress,
   396  			currentInterface.Dhcp4,
   397  			currentInterface.Dhcp6,
   398  			currentInterface.Gateway4,
   399  			currentInterface.Gateway6,
   400  			currentInterface.TerminalInterfaceID,
   401  		}
   402  
   403  		// update interface in db
   404  		if _, err = transaction.ExecContext(ctx, sqlquery.TerminalInterfaceUpdateQuery, args...); err != nil {
   405  			return err
   406  		}
   407  
   408  		// update addresses if necessary
   409  		if len(interfaceValues.Addresses) > 0 {
   410  			if err := t.updateTerminalAddressEntries(ctx, transaction, interfaceValues.Addresses, currentInterface.Addresses); err != nil {
   411  				return err
   412  			}
   413  		}
   414  
   415  		// validate all interface addresses after update
   416  		if err := utils.ValidateAllTerminalAddresses(currentInterface.Dhcp4, currentInterface.Addresses); err != nil {
   417  			return err
   418  		}
   419  	}
   420  	return nil
   421  }
   422  
   423  func (t *terminalService) updateTerminalAddressEntries(ctx context.Context, transaction *sql.Tx, updateAddresses []*model.TerminalAddressIDInput, currentAddresses []*model.TerminalAddress) error {
   424  	for _, updateAddress := range updateAddresses {
   425  		// update current address with new values
   426  		currentAddress, err := utils.UpdateTerminalAddress(currentAddresses, updateAddress)
   427  		if err != nil {
   428  			return err
   429  		}
   430  		// validate address
   431  		if err = utils.ValidateTerminalAddress(currentAddress); err != nil {
   432  			return err
   433  		}
   434  
   435  		args := []interface{}{
   436  			currentAddress.IP,
   437  			currentAddress.PrefixLen,
   438  			currentAddress.Family,
   439  			currentAddress.TerminalAddressID,
   440  		}
   441  
   442  		// update address in db
   443  		if _, err = transaction.ExecContext(ctx, sqlquery.TerminalAddressUpdateQuery, args...); err != nil {
   444  			return err
   445  		}
   446  	}
   447  	return nil
   448  }
   449  
   450  func (t *terminalService) DeleteTerminalEntry(ctx context.Context, terminalID string) error {
   451  	_, err := t.SQLDB.ExecContext(ctx, sqlquery.TerminalDeleteQuery, terminalID)
   452  	return err
   453  }
   454  
   455  func (t *terminalService) CreateTerminalInterfaceEntry(ctx context.Context, terminalID string, newTerminalInterface *model.TerminalInterfaceCreateInput) (*model.TerminalInterface, error) {
   456  	if err := utils.ValidateTerminalInterfaceCreateInput(newTerminalInterface); err != nil {
   457  		return nil, err
   458  	}
   459  
   460  	transaction, err := t.SQLDB.BeginTx(ctx, nil)
   461  	if err != nil {
   462  		return nil, err
   463  	}
   464  	newIfaceList := []*model.TerminalInterfaceCreateInput{newTerminalInterface}
   465  	ifaceList, err := t.createTerminalInterfaceEntries(ctx, transaction, newIfaceList, terminalID)
   466  	if err != nil {
   467  		if rollbackErr := transaction.Rollback(); rollbackErr != nil {
   468  			return nil, rollbackErr
   469  		}
   470  		return nil, err
   471  	}
   472  	if err = transaction.Commit(); err != nil {
   473  		return nil, err
   474  	}
   475  	if len(ifaceList) == 0 {
   476  		return nil, errors.New("no interface to return - potential error creating interface entry")
   477  	}
   478  	return ifaceList[0], nil
   479  }
   480  
   481  func (t *terminalService) DeleteTerminalInterfaceEntry(ctx context.Context, terminalInterfaceID string) (*model.Terminal, error) {
   482  	terminal, err := t.GetTerminalFromInterface(ctx, terminalInterfaceID)
   483  	if err != nil {
   484  		return nil, err
   485  	}
   486  
   487  	if len(terminal.Interfaces) == 1 {
   488  		return nil, errors.New("unable to delete the last interface of the terminal")
   489  	}
   490  
   491  	for i, iface := range terminal.Interfaces {
   492  		if iface.TerminalInterfaceID == terminalInterfaceID {
   493  			terminal.Interfaces[i] = terminal.Interfaces[len(terminal.Interfaces)-1]
   494  			terminal.Interfaces = terminal.Interfaces[:len(terminal.Interfaces)-1]
   495  			break
   496  		}
   497  	}
   498  
   499  	_, err = t.SQLDB.ExecContext(ctx, sqlquery.TerminalInterfaceDeleteQuery, terminalInterfaceID)
   500  	return terminal, err
   501  }
   502  
   503  func (t *terminalService) CreateTerminalAddressEntry(ctx context.Context, terminalInterfaceID string, newTerminalAddress *model.TerminalAddressCreateInput) (*model.TerminalAddress, error) {
   504  	address := utils.CreateTerminalAddressModel(uuid.NewString(), newTerminalAddress.IP, newTerminalAddress.PrefixLen, newTerminalAddress.Family, terminalInterfaceID)
   505  	args := []interface{}{
   506  		address.TerminalAddressID,
   507  		address.IP,
   508  		address.PrefixLen,
   509  		address.Family,
   510  		address.TerminalInterfaceID,
   511  	}
   512  	_, err := t.SQLDB.ExecContext(ctx, sqlquery.TerminalAddressCreateQuery, args...)
   513  	if err != nil {
   514  		return nil, err
   515  	}
   516  	return &address, nil
   517  }
   518  
   519  func (t *terminalService) UpdateTerminalAddressEntry(ctx context.Context, updatedAddress *model.TerminalAddressIDInput) (*model.TerminalAddress, error) { //nolint
   520  	updateAddresses := []*model.TerminalAddressIDInput{updatedAddress}
   521  	currentAddr, err := t.getTerminalAddress(ctx, updatedAddress.TerminalAddressID)
   522  	if err != nil {
   523  		return nil, err
   524  	}
   525  	currentAddrs := []*model.TerminalAddress{currentAddr}
   526  
   527  	transaction, err := t.SQLDB.BeginTx(ctx, nil)
   528  	if err != nil {
   529  		return nil, err
   530  	}
   531  
   532  	if err := t.updateTerminalAddressEntries(ctx, transaction, updateAddresses, currentAddrs); err != nil {
   533  		if rollbackErr := transaction.Rollback(); rollbackErr != nil {
   534  			return nil, rollbackErr
   535  		}
   536  		return nil, err
   537  	}
   538  
   539  	if err := transaction.Commit(); err != nil {
   540  		return nil, err
   541  	}
   542  
   543  	addr, err := t.getTerminalAddress(ctx, updatedAddress.TerminalAddressID)
   544  	if err != nil {
   545  		return nil, err
   546  	}
   547  	return addr, nil
   548  }
   549  
   550  func (t *terminalService) DeleteTerminalAddressEntry(ctx context.Context, terminalAddressID string) (*model.Terminal, error) {
   551  	terminal, err := t.GetTerminalFromAddress(ctx, terminalAddressID)
   552  	if err != nil {
   553  		return nil, err
   554  	}
   555  
   556  out:
   557  	for _, iface := range terminal.Interfaces {
   558  		for i, address := range iface.Addresses {
   559  			if address.TerminalAddressID == terminalAddressID {
   560  				iface.Addresses[i] = iface.Addresses[len(iface.Addresses)-1]
   561  				iface.Addresses = iface.Addresses[:len(iface.Addresses)-1]
   562  
   563  				// validate that the address can be deleted
   564  				if err := utils.ValidateAllTerminalAddresses(iface.Dhcp4, iface.Addresses); err != nil {
   565  					return nil, err
   566  				}
   567  				break out
   568  			}
   569  		}
   570  	}
   571  
   572  	_, err = t.SQLDB.ExecContext(ctx, sqlquery.TerminalAddressDeleteQuery, terminalAddressID)
   573  	return terminal, err
   574  }
   575  
   576  func (t *terminalService) GetTerminal(ctx context.Context, terminalID string, getLabel *bool) (*model.Terminal, error) {
   577  	row := t.SQLDB.QueryRowContext(ctx, sqlquery.GetTerminalByIDQuery, terminalID)
   578  	terminal, err := t.scanTerminalRow(row)
   579  	if err != nil {
   580  		return nil, err
   581  	}
   582  
   583  	terminal.Interfaces, err = t.getTerminalInterfaceByTerminalID(ctx, &terminalID)
   584  	if err != nil {
   585  		return nil, err
   586  	}
   587  
   588  	terminal.Disks, err = t.getTerminalDisks(ctx, &terminalID)
   589  	if err != nil {
   590  		return nil, err
   591  	}
   592  
   593  	if getLabel != nil && *getLabel {
   594  		terminalLabelSvc := NewTerminalLabelService(t.SQLDB, nil, nil, nil, t.LabelService)
   595  		terminalLabels, err := terminalLabelSvc.GetTerminalLabels(ctx, model.SearchTerminalLabelInput{
   596  			TerminalID: &terminalID,
   597  		})
   598  		if err != nil {
   599  			return nil, err
   600  		}
   601  		terminal.Labels = terminalLabels
   602  	}
   603  
   604  	term, err := t.updateTerminalVersions(ctx, []*model.Terminal{terminal})
   605  	if err != nil {
   606  		return nil, err
   607  	}
   608  	terminal = term[0]
   609  
   610  	return terminal, nil
   611  }
   612  
   613  func (t *terminalService) GetTerminals(ctx context.Context, clusterEdgeID *string, terminalHostname *string) ([]*model.Terminal, error) {
   614  	switch {
   615  	case clusterEdgeID == nil && terminalHostname == nil:
   616  		return t.getAllTerminals(ctx)
   617  	case clusterEdgeID != nil && terminalHostname == nil:
   618  		return t.getClusterEdgeIDTerminals(ctx, *clusterEdgeID)
   619  	case clusterEdgeID == nil && terminalHostname != nil:
   620  		return t.getHostnameTerminals(ctx, *terminalHostname)
   621  	default:
   622  		return t.getClusterEdgeIDAndHostnameTerminals(ctx, *clusterEdgeID, *terminalHostname)
   623  	}
   624  }
   625  
   626  func (t *terminalService) GetNodeReplicationStatus(ctx context.Context, clusterEdgeID, hostname string) (*model.ReplicationStatus, error) {
   627  	var name string
   628  	row := t.SQLDB.QueryRowContext(ctx, sqlquery.GetNodeReplicaName, clusterEdgeID, fmt.Sprintf("%q", hostname))
   629  	if err := row.Scan(&name); err != nil {
   630  		if errors.Is(err, sql.ErrNoRows) {
   631  			return &model.ReplicationStatus{
   632  				Status: "NotFound",
   633  			}, nil
   634  		}
   635  		return nil, sqlerr.Wrap(err)
   636  	}
   637  	row = t.SQLDB.QueryRowContext(ctx, sqlquery.GetReplicationStatusByName, clusterEdgeID, name)
   638  	var replicationStatus model.ReplicationStatus
   639  	if err := row.Scan(&replicationStatus.Name, &replicationStatus.Status); err != nil {
   640  		if errors.Is(err, sql.ErrNoRows) {
   641  			return &model.ReplicationStatus{
   642  				Status: "NotFound",
   643  			}, nil
   644  		}
   645  		return nil, sqlerr.Wrap(err)
   646  	}
   647  	replicationStatus.Status = strings.Trim(replicationStatus.Status, "\"")
   648  	return &replicationStatus, nil
   649  }
   650  
   651  func (t *terminalService) GetTerminalNodeStatus(ctx context.Context, clusterEdgeID, terminalID, hostname string) (*model.TerminalStatus, error) {
   652  	var (
   653  		nodeReady      = ""
   654  		ieNodeReady    = ""
   655  		terminalStatus = &model.TerminalStatus{}
   656  		notReported    = make(map[string]bool)
   657  	)
   658  	row := t.SQLDB.QueryRowContext(ctx, sqlquery.GetTerminalNodeStatus, "Node", clusterEdgeID, hostname, false)
   659  	err := row.Scan(&nodeReady)
   660  	if err != nil && !errors.Is(err, sql.ErrNoRows) {
   661  		return nil, err
   662  	}
   663  	// Handling cases where the Kubernetes Node Status is reported as UNKNOWN
   664  	// We are treating this as being not reported
   665  	if nodeReady == status.Unknown || errors.Is(err, sql.ErrNoRows) {
   666  		notReported["Node"] = true
   667  	}
   668  	row = t.SQLDB.QueryRowContext(ctx, sqlquery.GetTerminalNodeStatus, v1ien.IENodeGVK.Kind, clusterEdgeID, hostname, false)
   669  	if err := row.Scan(&ieNodeReady); err != nil {
   670  		if !errors.Is(err, sql.ErrNoRows) {
   671  			return nil, err
   672  		}
   673  		notReported[v1ien.IENodeGVK.Kind] = true
   674  	}
   675  	// Case 1: Node Status = True, Node Agent (IENode) Status = True, and (Activation Code Consumed = True || Activation Code Consumed = False)
   676  	// @returns status  = Ready
   677  	// @returns message = Node Ready
   678  	if nodeReady == status.IsReady && ieNodeReady == status.IsReady {
   679  		terminalStatus.Status = status.Ready
   680  		terminalStatus.Message = status.NodeReadyMessage
   681  		return terminalStatus, nil
   682  	}
   683  	// Case 2: Node Status = Error, Node Agent (IENode) Status = Error
   684  	// @returns status  = Error
   685  	// @returns message = Node status + node agent status condition message
   686  	if nodeReady == status.NotReady || ieNodeReady == status.NotReady { //nolint:nestif
   687  		aggregatedErr := make([]string, 0)
   688  		if nodeReady == status.NotReady {
   689  			nodeErrMessage := ""
   690  			if err := t.getNodeErrorMessage(ctx, clusterEdgeID, "Node", hostname, &nodeErrMessage, &aggregatedErr); err != nil {
   691  				return nil, err
   692  			}
   693  		}
   694  		if ieNodeReady == status.NotReady {
   695  			ieNodeErrMessage := ""
   696  			if err := t.getNodeErrorMessage(ctx, clusterEdgeID, v1ien.IENodeGVK.Kind, hostname, &ieNodeErrMessage, &aggregatedErr); err != nil {
   697  				return nil, err
   698  			}
   699  		}
   700  		terminalStatus.Status = status.Error
   701  		terminalStatus.Message = status.MergeErrorMessages(aggregatedErr)
   702  		return terminalStatus, nil
   703  	}
   704  	activationCode := ""
   705  	row = t.SQLDB.QueryRowContext(ctx, edgenode.GetActivationCode, terminalID)
   706  	if err := row.Scan(&activationCode); err != nil {
   707  		return nil, err
   708  	}
   709  	if status.IsNotReported("Node", notReported) || status.IsNotReported(v1ien.IENodeGVK.Kind, notReported) {
   710  		switch {
   711  		case status.IsNotReported("Node", notReported) && status.IsNotReported(v1ien.IENodeGVK.Kind, notReported) && activationCode == "":
   712  			// Case 3: Node Status = N/A, Node Agent Status (IENode) = N/A, and Activation Code Consumed = True
   713  			// @returns status  = N/A
   714  			// @returns message = Status for node missing
   715  			terminalStatus.Status = status.NotAvailable
   716  			terminalStatus.Message = status.NotAvailableMessage
   717  		case activationCode != "":
   718  			// Case 4: Node Status = N/A, Node Agent (IENode) Status = N/A, and Activation Code Consumed = False
   719  			// @returns status  = Awaiting Installation
   720  			// @returns message = Activation code not consumed
   721  			terminalStatus.Status = status.AwaitingInstallation
   722  			terminalStatus.Message = status.UnusedActivationCodeMessage
   723  			// Case 5: Node Status = Unknown, Node Agent Status (IENode) = "", and Activation Code Consumed = True
   724  			// @returns status  = Disconnected
   725  			// @returns message = Node is disconnected or turned off
   726  		case activationCode == "":
   727  			terminalStatus.Status = status.Disconnected
   728  			terminalStatus.Message = status.DisconnectedMessage
   729  		}
   730  	}
   731  	return terminalStatus, nil
   732  }
   733  
   734  func (t *terminalService) getNodeErrorMessage(ctx context.Context, clusterEdgeID, kind, hostName string, errMessage *string, aggregatedErr *[]string) error {
   735  	row := t.SQLDB.QueryRowContext(ctx, sqlquery.GetTerminalNodeStatusMessage, kind, clusterEdgeID, hostName, false)
   736  	if err := row.Scan(errMessage); err != nil {
   737  		if !errors.Is(err, sql.ErrNoRows) {
   738  			return err
   739  		}
   740  	} else {
   741  		*aggregatedErr = append(*aggregatedErr, *errMessage)
   742  	}
   743  	return nil
   744  }
   745  
   746  func (t *terminalService) GetBannerTerminals(ctx context.Context, bannerEdgeID string, projectID string) ([]*model.Terminal, error) {
   747  	// get all cluster edge ids for the banner
   748  	rows, err := t.SQLDB.QueryContext(ctx, sqlquery.GetClustersByBannerEdgeIDQuery, bannerEdgeID)
   749  	if err != nil {
   750  		return nil, err
   751  	}
   752  
   753  	// version information will eventually be returned by the above query once ctlfish moves over to the SQLDB
   754  	versionMap, err := t.getTerminalVersionMapFromProjectID(ctx, projectID)
   755  	if err != nil {
   756  		return nil, err
   757  	}
   758  
   759  	terminalMap := make(map[string]*model.Terminal)
   760  	ifaceMap := make(map[string]*model.TerminalInterface)
   761  	addressMap := make(map[string]*model.TerminalAddress)
   762  
   763  	for rows.Next() {
   764  		terminal, ifaceBanner, addressBanner := model.Terminal{}, utils.NullIface{}, utils.NullAddress{}
   765  		err := rows.Scan(&terminal.TerminalID, &terminal.Lane, &terminal.Role, &terminal.Class, &terminal.DiscoverDisks, &terminal.BootDisk, &terminal.PrimaryInterface, &terminal.ExistingEfiPart, &terminal.SwapEnabled, &terminal.ClusterEdgeID, &terminal.ClusterName, &terminal.Hostname,
   766  			&ifaceBanner.TerminalInterfaceID, &ifaceBanner.MacAddress, &ifaceBanner.Dhcp4, &ifaceBanner.Dhcp6, &ifaceBanner.Gateway4, &ifaceBanner.Gateway6, &ifaceBanner.TerminalID,
   767  			&addressBanner.TerminalAddressID, &addressBanner.IP, &addressBanner.PrefixLen, &addressBanner.Family, &addressBanner.TerminalInterfaceID)
   768  		if err != nil {
   769  			return nil, err
   770  		}
   771  
   772  		if _, exists := terminalMap[terminal.TerminalID]; !exists {
   773  			terminalMap[terminal.TerminalID] = &terminal
   774  		}
   775  		if ifaceBanner.TerminalInterfaceID.Valid {
   776  			iface, err := utils.ConvertNullIface(ifaceBanner)
   777  			if err != nil {
   778  				return nil, err
   779  			}
   780  			if _, exists := ifaceMap[iface.TerminalInterfaceID]; !exists {
   781  				ifaceMap[iface.TerminalInterfaceID] = iface
   782  			}
   783  		}
   784  		if addressBanner.TerminalAddressID.Valid {
   785  			address, err := utils.ConvertNullAddr(addressBanner)
   786  			if err != nil {
   787  				return nil, err
   788  			}
   789  			if _, exists := addressMap[address.TerminalAddressID]; !exists {
   790  				addressMap[address.TerminalAddressID] = address
   791  			}
   792  		}
   793  	}
   794  
   795  	if err := rows.Err(); err != nil {
   796  		return nil, sqlerr.Wrap(err)
   797  	}
   798  
   799  	terminals := mapper.UnpackBannerTerminals(terminalMap, ifaceMap, addressMap, versionMap)
   800  	for _, terminal := range terminals {
   801  		terminalDisks, err := t.getTerminalDisks(ctx, &terminal.TerminalID)
   802  		if err != nil {
   803  			return nil, err
   804  		}
   805  		terminal.Disks = terminalDisks
   806  	}
   807  
   808  	return terminals, nil
   809  }
   810  
   811  func (t *terminalService) getAllTerminals(ctx context.Context) ([]*model.Terminal, error) {
   812  	rows, err := t.SQLDB.QueryContext(ctx, sqlquery.GetAllTerminalsQuery)
   813  	if err != nil {
   814  		return nil, err
   815  	}
   816  
   817  	terminals, err := t.scanTerminalRows(rows)
   818  	if err != nil {
   819  		return nil, err
   820  	}
   821  
   822  	for _, terminal := range terminals {
   823  		terminalDisks, err := t.getTerminalDisks(ctx, &terminal.TerminalID)
   824  		if err != nil {
   825  			return nil, err
   826  		}
   827  		terminal.Disks = terminalDisks
   828  
   829  		terminal.Interfaces, err = t.getTerminalInterfaceByTerminalID(ctx, &terminal.TerminalID)
   830  		if err != nil {
   831  			return nil, err
   832  		}
   833  
   834  		terminalLabelSvc := NewTerminalLabelService(t.SQLDB, nil, nil, nil, t.LabelService)
   835  		terminalLabels, err := terminalLabelSvc.GetTerminalLabels(ctx, model.SearchTerminalLabelInput{
   836  			TerminalID: &terminal.TerminalID,
   837  		})
   838  		if err != nil {
   839  			return nil, err
   840  		}
   841  		terminal.Labels = terminalLabels
   842  	}
   843  
   844  	return t.updateTerminalVersions(ctx, terminals)
   845  }
   846  
   847  func (t *terminalService) getClusterEdgeIDTerminals(ctx context.Context, clusterEdgeID string) ([]*model.Terminal, error) { //nolint
   848  	terminals, err := t.GetTerminalsByClusterID(ctx, clusterEdgeID)
   849  	if err != nil {
   850  		return nil, err
   851  	}
   852  
   853  	for _, terminal := range terminals {
   854  		terminalDisks, err := t.getTerminalDisks(ctx, &terminal.TerminalID)
   855  		if err != nil {
   856  			return nil, err
   857  		}
   858  		terminal.Disks = terminalDisks
   859  
   860  		terminal.Interfaces, err = t.getTerminalInterfaceByTerminalID(ctx, &terminal.TerminalID)
   861  		if err != nil {
   862  			return nil, err
   863  		}
   864  
   865  		terminalLabelSvc := NewTerminalLabelService(t.SQLDB, nil, nil, nil, t.LabelService)
   866  		terminalLabels, err := terminalLabelSvc.GetTerminalLabels(ctx, model.SearchTerminalLabelInput{
   867  			TerminalID: &terminal.TerminalID,
   868  		})
   869  		if err != nil {
   870  			return nil, err
   871  		}
   872  		terminal.Labels = terminalLabels
   873  	}
   874  
   875  	return t.updateTerminalVersions(ctx, terminals)
   876  }
   877  
   878  func (t *terminalService) GetTerminalsByClusterID(ctx context.Context, clusterEdgeID string) ([]*model.Terminal, error) {
   879  	rows, err := t.SQLDB.QueryContext(ctx, sqlquery.GetTerminalByClusterEdgeIDQuery, clusterEdgeID)
   880  	if err != nil {
   881  		return nil, err
   882  	}
   883  
   884  	terminals, err := t.scanTerminalRows(rows)
   885  	if err != nil {
   886  		return nil, err
   887  	}
   888  	return terminals, nil
   889  }
   890  
   891  func (t *terminalService) getHostnameTerminals(ctx context.Context, terminalHostname string) ([]*model.Terminal, error) { //nolint
   892  	rows, err := t.SQLDB.QueryContext(ctx, sqlquery.GetTerminalByHostnameQuery, terminalHostname)
   893  	if err != nil {
   894  		return nil, err
   895  	}
   896  
   897  	terminals, err := t.scanTerminalRows(rows)
   898  	if err != nil {
   899  		return nil, err
   900  	}
   901  
   902  	for _, terminal := range terminals {
   903  		terminalDisks, err := t.getTerminalDisks(ctx, &terminal.TerminalID)
   904  		if err != nil {
   905  			return nil, err
   906  		}
   907  		terminal.Disks = terminalDisks
   908  
   909  		terminal.Interfaces, err = t.getTerminalInterfaceByTerminalID(ctx, &terminal.TerminalID)
   910  		if err != nil {
   911  			return nil, err
   912  		}
   913  
   914  		terminalLabelSvc := NewTerminalLabelService(t.SQLDB, nil, nil, nil, t.LabelService)
   915  		terminalLabels, err := terminalLabelSvc.GetTerminalLabels(ctx, model.SearchTerminalLabelInput{
   916  			TerminalID: &terminal.TerminalID,
   917  		})
   918  		if err != nil {
   919  			return nil, err
   920  		}
   921  		terminal.Labels = terminalLabels
   922  	}
   923  
   924  	return t.updateTerminalVersions(ctx, terminals)
   925  }
   926  
   927  func (t *terminalService) getClusterEdgeIDAndHostnameTerminals(ctx context.Context, clusterEdgeID string, terminalHostname string) ([]*model.Terminal, error) {
   928  	rows, err := t.SQLDB.QueryContext(ctx, sqlquery.GetTerminalByClusterEdgeIDAndHostnameQuery, clusterEdgeID, terminalHostname)
   929  	if err != nil {
   930  		return nil, err
   931  	}
   932  
   933  	terminals, err := t.scanTerminalRows(rows)
   934  	if err != nil {
   935  		return nil, err
   936  	}
   937  
   938  	for _, terminal := range terminals {
   939  		terminalDisks, err := t.getTerminalDisks(ctx, &terminal.TerminalID)
   940  		if err != nil {
   941  			return nil, err
   942  		}
   943  		terminal.Disks = terminalDisks
   944  
   945  		terminal.Interfaces, err = t.getTerminalInterfaceByTerminalID(ctx, &terminal.TerminalID)
   946  		if err != nil {
   947  			return nil, err
   948  		}
   949  
   950  		terminalLabelSvc := NewTerminalLabelService(t.SQLDB, nil, nil, nil, t.LabelService)
   951  		terminalLabels, err := terminalLabelSvc.GetTerminalLabels(ctx, model.SearchTerminalLabelInput{
   952  			TerminalID: &terminal.TerminalID,
   953  		})
   954  		if err != nil {
   955  			return nil, err
   956  		}
   957  		terminal.Labels = terminalLabels
   958  	}
   959  
   960  	return t.updateTerminalVersions(ctx, terminals)
   961  }
   962  
   963  func (t *terminalService) getTerminalInterface(ctx context.Context, interfaceID string) (*model.TerminalInterface, error) {
   964  	row := t.SQLDB.QueryRowContext(ctx, sqlquery.GetTerminalInterfaceQuery, interfaceID)
   965  	iface, err := t.scanTerminalIfaceRow(row)
   966  	if err != nil {
   967  		return nil, err
   968  	}
   969  
   970  	iface.Addresses, err = t.getTerminalAddressByInterfaceID(ctx, &iface.TerminalInterfaceID)
   971  	if err != nil {
   972  		return nil, err
   973  	}
   974  
   975  	return iface, nil
   976  }
   977  
   978  func (t *terminalService) getTerminalInterfaceByTerminalID(ctx context.Context, terminalID *string) ([]*model.TerminalInterface, error) {
   979  	rows, err := t.SQLDB.QueryContext(ctx, sqlquery.GetTerminalInterfaceByTerminalIDQuery, terminalID)
   980  	if err != nil {
   981  		return nil, err
   982  	}
   983  
   984  	ifaces, err := t.scanTerminalIfaceRows(rows)
   985  	if err != nil {
   986  		return nil, err
   987  	}
   988  
   989  	for _, iface := range ifaces {
   990  		iface.Addresses, err = t.getTerminalAddressByInterfaceID(ctx, &iface.TerminalInterfaceID)
   991  		if err != nil {
   992  			return nil, err
   993  		}
   994  	}
   995  
   996  	return ifaces, nil
   997  }
   998  
   999  func (t *terminalService) getTerminalAddress(ctx context.Context, addressID string) (*model.TerminalAddress, error) {
  1000  	row := t.SQLDB.QueryRowContext(ctx, sqlquery.GetTerminalAddressQuery, addressID)
  1001  	addr, err := t.scanTerminalAddressRow(row)
  1002  	if err != nil {
  1003  		return nil, err
  1004  	}
  1005  	return addr, nil
  1006  }
  1007  
  1008  func (t *terminalService) getTerminalAddressByInterfaceID(ctx context.Context, terminalInterfaceID *string) ([]*model.TerminalAddress, error) {
  1009  	rows, err := t.SQLDB.QueryContext(ctx, sqlquery.GetTerminalAddressByInterfaceIDQuery, terminalInterfaceID)
  1010  	if err != nil {
  1011  		return nil, err
  1012  	}
  1013  
  1014  	addresses, err := t.scanTerminalAddressRows(rows)
  1015  	if err != nil {
  1016  		return nil, err
  1017  	}
  1018  
  1019  	return addresses, nil
  1020  }
  1021  
  1022  func (t *terminalService) scanTerminalRow(row *sql.Row) (*model.Terminal, error) {
  1023  	terminal := &model.Terminal{}
  1024  	err := row.Scan(&terminal.TerminalID, &terminal.Lane, &terminal.Role, &terminal.ClusterEdgeID, &terminal.ClusterName, &terminal.Class, &terminal.DiscoverDisks, &terminal.BootDisk, &terminal.PrimaryInterface, &terminal.ExistingEfiPart, &terminal.SwapEnabled, &terminal.Hostname)
  1025  	if err != nil {
  1026  		return nil, err
  1027  	}
  1028  	return terminal, nil
  1029  }
  1030  
  1031  func (t *terminalService) scanTerminalRows(rows *sql.Rows) ([]*model.Terminal, error) {
  1032  	terminals := []*model.Terminal{}
  1033  	for rows.Next() {
  1034  		terminal := &model.Terminal{}
  1035  		err := rows.Scan(&terminal.TerminalID, &terminal.Lane, &terminal.Role, &terminal.ClusterEdgeID, &terminal.ClusterName, &terminal.Class, &terminal.DiscoverDisks, &terminal.BootDisk, &terminal.PrimaryInterface, &terminal.ExistingEfiPart, &terminal.SwapEnabled, &terminal.Hostname)
  1036  		if err != nil {
  1037  			return nil, err
  1038  		}
  1039  		terminals = append(terminals, terminal)
  1040  	}
  1041  	if err := rows.Err(); err != nil {
  1042  		return nil, sqlerr.Wrap(err)
  1043  	}
  1044  	return terminals, nil
  1045  }
  1046  
  1047  func (t *terminalService) scanTerminalIfaceRow(row *sql.Row) (*model.TerminalInterface, error) {
  1048  	iface := &model.TerminalInterface{}
  1049  	if err := row.Scan(&iface.TerminalInterfaceID, &iface.MacAddress, &iface.Dhcp4, &iface.Dhcp6, &iface.Gateway4, &iface.Gateway6, &iface.TerminalID); err != nil {
  1050  		return nil, err
  1051  	}
  1052  	return iface, nil
  1053  }
  1054  
  1055  func (t *terminalService) scanTerminalIfaceRows(rows *sql.Rows) ([]*model.TerminalInterface, error) {
  1056  	terminalInterfaces := []*model.TerminalInterface{}
  1057  	for rows.Next() {
  1058  		terminalInterface := model.TerminalInterface{}
  1059  		err := rows.Scan(&terminalInterface.TerminalInterfaceID, &terminalInterface.MacAddress, &terminalInterface.Dhcp4, &terminalInterface.Dhcp6, &terminalInterface.Gateway4, &terminalInterface.Gateway6, &terminalInterface.TerminalID)
  1060  		if err != nil {
  1061  			return nil, err
  1062  		}
  1063  		terminalInterfaces = append(terminalInterfaces, &terminalInterface)
  1064  	}
  1065  	if err := rows.Err(); err != nil {
  1066  		return nil, sqlerr.Wrap(err)
  1067  	}
  1068  	return terminalInterfaces, nil
  1069  }
  1070  
  1071  func (t *terminalService) scanTerminalAddressRow(row *sql.Row) (*model.TerminalAddress, error) {
  1072  	addr := &model.TerminalAddress{}
  1073  	if err := row.Scan(&addr.TerminalAddressID, &addr.IP, &addr.PrefixLen, &addr.Family, &addr.TerminalInterfaceID); err != nil {
  1074  		return nil, err
  1075  	}
  1076  	return addr, nil
  1077  }
  1078  
  1079  func (t *terminalService) scanTerminalAddressRows(rows *sql.Rows) ([]*model.TerminalAddress, error) {
  1080  	terminalAddresses := []*model.TerminalAddress{}
  1081  	for rows.Next() {
  1082  		terminalAddress := model.TerminalAddress{}
  1083  		if err := rows.Scan(&terminalAddress.TerminalAddressID, &terminalAddress.IP, &terminalAddress.PrefixLen, &terminalAddress.Family, &terminalAddress.TerminalInterfaceID); err != nil {
  1084  			return nil, err
  1085  		}
  1086  		terminalAddresses = append(terminalAddresses, &terminalAddress)
  1087  	}
  1088  	if err := rows.Err(); err != nil {
  1089  		return nil, sqlerr.Wrap(err)
  1090  	}
  1091  	return terminalAddresses, nil
  1092  }
  1093  
  1094  func (t *terminalService) CreateDSDSIENodeCR(terminal *model.Terminal, clusterNetworkServices []*model.ClusterNetworkServiceInfo, customLabels map[string]string, edgeVersion string) (string, error) {
  1095  	dsdsIENode := mapper.TerminalToIENode(terminal, clusterNetworkServices, customLabels, edgeVersion)
  1096  	dsdsIENodeBase64, err := utils.ConvertStructToBase64(dsdsIENode)
  1097  	if err != nil {
  1098  		return "", err
  1099  	}
  1100  	return dsdsIENodeBase64, nil
  1101  }
  1102  
  1103  func (t *terminalService) CreateCICIENodeCR(terminal *model.Terminal, clusterNetworkServices []*model.ClusterNetworkServiceInfo, customLabels map[string]string, edgeVersion string) (string, error) {
  1104  	terminalCR := mapper.TerminalToCICIENode(terminal, clusterNetworkServices, customLabels, edgeVersion)
  1105  	terminalCRBase64, err := utils.ConvertStructToBase64(terminalCR)
  1106  	if err != nil {
  1107  		return "", err
  1108  	}
  1109  	return terminalCRBase64, nil
  1110  }
  1111  
  1112  func (t *terminalService) GetTerminalFromInterface(ctx context.Context, terminalInterfaceID string) (*model.Terminal, error) {
  1113  	row := t.SQLDB.QueryRowContext(ctx, sqlquery.GetTerminalIDFromInterfaceQuery, terminalInterfaceID)
  1114  	var terminalID string
  1115  	if err := row.Scan(&terminalID); err != nil {
  1116  		return nil, err
  1117  	}
  1118  
  1119  	terminal, err := t.GetTerminal(ctx, terminalID, &getLabel)
  1120  	if err != nil {
  1121  		return nil, err
  1122  	}
  1123  
  1124  	return terminal, nil
  1125  }
  1126  
  1127  func (t *terminalService) GetTerminalFromAddress(ctx context.Context, terminalAddressID string) (*model.Terminal, error) {
  1128  	row := t.SQLDB.QueryRowContext(ctx, sqlquery.GetInterfaceIDFromAddressQuery, terminalAddressID)
  1129  	var ifaceID string
  1130  	if err := row.Scan(&ifaceID); err != nil {
  1131  		return nil, err
  1132  	}
  1133  
  1134  	row = t.SQLDB.QueryRowContext(ctx, sqlquery.GetTerminalIDFromInterfaceQuery, ifaceID)
  1135  	var terminalID string
  1136  	if err := row.Scan(&terminalID); err != nil {
  1137  		return nil, err
  1138  	}
  1139  
  1140  	terminal, err := t.GetTerminal(ctx, terminalID, &getLabel)
  1141  	if err != nil {
  1142  		return nil, err
  1143  	}
  1144  
  1145  	return terminal, nil
  1146  }
  1147  
  1148  func (t *terminalService) TerminalDevices(ctx context.Context, terminalID string) (*model.TerminalDevices, error) {
  1149  	fetchLabels := false
  1150  	terminal, err := t.GetTerminal(ctx, terminalID, &fetchLabels)
  1151  	if err != nil {
  1152  		return &model.TerminalDevices{}, err
  1153  	}
  1154  
  1155  	var value string
  1156  	row := t.SQLDB.QueryRowContext(ctx, sqlquery.GetTerminalDevices, terminal.Hostname, terminal.ClusterEdgeID)
  1157  	if err := row.Scan(&value); err != nil {
  1158  		return &model.TerminalDevices{}, err
  1159  	}
  1160  
  1161  	deviceMap := dsv1.DeviceStatusesSpec{Devices: map[string][]dsv1.DeviceState{}}
  1162  	if err = json.Unmarshal([]byte(value), &deviceMap.Devices); err != nil {
  1163  		return &model.TerminalDevices{}, fmt.Errorf("failed to convert device map: %v", err)
  1164  	}
  1165  
  1166  	terminalDevices := &model.TerminalDevices{Classes: make([]*model.ClassDeviceMap, len(deviceMap.Devices))}
  1167  
  1168  	classIdx := 0
  1169  	for class, classDevices := range deviceMap.Devices {
  1170  		devices := make([]*model.Device, len(classDevices))
  1171  		devIdx := 0
  1172  		for _, dev := range classDevices {
  1173  			devices[devIdx] = &model.Device{Name: dev.Name}
  1174  			devIdx++
  1175  		}
  1176  
  1177  		terminalDevices.Classes[classIdx] = &model.ClassDeviceMap{
  1178  			Name:    class,
  1179  			Devices: devices,
  1180  		}
  1181  		classIdx++
  1182  	}
  1183  	return terminalDevices, nil
  1184  }
  1185  
  1186  func (t *terminalService) checkHostnames(ctx context.Context, nodeName string, newTerminal *model.TerminalCreateInput) error {
  1187  	// get all of the hostnames in the cluster
  1188  	rows, err := t.SQLDB.QueryContext(ctx, sqlquery.GetAllHostnamesForAClusterQuery, newTerminal.ClusterEdgeID)
  1189  	if err != nil {
  1190  		return err
  1191  	}
  1192  
  1193  	var hostname string
  1194  	for rows.Next() {
  1195  		err := rows.Scan(&hostname)
  1196  		if err != nil {
  1197  			return err
  1198  		}
  1199  		if hostname == nodeName {
  1200  			return fmt.Errorf("cannot create terminal - hostname %s already exists", hostname)
  1201  		}
  1202  	}
  1203  	if err := rows.Err(); err != nil {
  1204  		return sqlerr.Wrap(err)
  1205  	}
  1206  	return nil
  1207  }
  1208  
  1209  func (t *terminalService) RemoveClusterEdgeBootstrapToken(ctx context.Context, clusterEdgeID string) error {
  1210  	if clusterEdgeID == "" {
  1211  		return nil
  1212  	}
  1213  	return t.uploadClusterEdgeBootstrapToken(ctx, "", clusterEdgeID)
  1214  }
  1215  
  1216  func (t *terminalService) uploadClusterEdgeBootstrapToken(ctx context.Context, edgeBootstrapTokenHash, clusterEdgeID string) error {
  1217  	expirationTime := time.Now().UTC().Add(time.Hour * 2).Format(mapper.TimeFormat)
  1218  	_, err := t.SQLDB.ExecContext(ctx, sqlquery.UploadEdgeBootstrapToken, edgeBootstrapTokenHash, expirationTime, clusterEdgeID)
  1219  	return err
  1220  }
  1221  
  1222  func (t *terminalService) checkLaneDuplicates(ctx context.Context, newLane, clusterEdgeID string) error {
  1223  	// check that a lane is unique for a cluster
  1224  	rows, err := t.SQLDB.QueryContext(ctx, sqlquery.GetAllLanesForAClusterQuery, clusterEdgeID)
  1225  	if err != nil {
  1226  		return err
  1227  	}
  1228  
  1229  	for rows.Next() {
  1230  		var hostname string
  1231  		var lane sql.NullString
  1232  		if err := rows.Scan(&lane, &hostname); err != nil {
  1233  			return err
  1234  		}
  1235  		if lane.Valid && lane.String == newLane {
  1236  			return fmt.Errorf("cannot create terminal - lane %s already in use by terminal %s", newLane, hostname)
  1237  		}
  1238  	}
  1239  	if err := rows.Err(); err != nil {
  1240  		return sqlerr.Wrap(err)
  1241  	}
  1242  	return nil
  1243  }
  1244  
  1245  func (t *terminalService) GetTerminalBootstrapConfig(ctx context.Context, terminal *model.Terminal, clusterNetworkServices []*model.ClusterNetworkServiceInfo, breakGlassSecret, grubSecret cc.Secret, bootstrapAck, isFirstNode bool, organization string, bootstrapTokenValues []*model.KeyValues, clusterCaHash, endpoint string) (string, error) {
  1246  	edgeInfraVersion := version.New().SemVer
  1247  
  1248  	grubEntry := strings.Split(grubSecret.String(), "\n")
  1249  	if len(grubEntry) != 3 {
  1250  		return "", errors.New("invalid grub credential returned to bootstrap config")
  1251  	}
  1252  
  1253  	grubLine := strings.Split(grubEntry[1], " ")
  1254  	if len(grubEntry) != 3 {
  1255  		return "", errors.New("invalid grub hash returned to bootstrap config")
  1256  	}
  1257  
  1258  	payload := mapper.TerminalBootstrapPayload{
  1259  		Terminal:               terminal,
  1260  		EdgeInfraVersion:       edgeInfraVersion,
  1261  		ClusterNetworkServices: clusterNetworkServices,
  1262  		ShadowFileLine:         breakGlassSecret.String(),
  1263  		GrubFileEntry:          grubLine[2],
  1264  		FirstNode:              isFirstNode,
  1265  		BootstrapTokenValues:   bootstrapTokenValues,
  1266  		ClusterCaHash:          clusterCaHash,
  1267  		BootstrapAck:           bootstrapAck,
  1268  		Organization:           organization,
  1269  		Endpoint:               endpoint,
  1270  	}
  1271  
  1272  	if isFirstNode {
  1273  		edgeBootstrapToken, err := crypto.GenerateRandomEdgeBootstrapToken()
  1274  		if err != nil {
  1275  			return "", err
  1276  		}
  1277  		if err := t.uploadClusterEdgeBootstrapToken(ctx, hex.EncodeToString(edgeBootstrapToken.Hashed()), terminal.ClusterEdgeID); err != nil {
  1278  			return "", err
  1279  		}
  1280  		payload.EdgeBootstrapToken = edgeBootstrapToken.Plain()
  1281  	}
  1282  
  1283  	config, err := payload.Yaml()
  1284  	if err != nil {
  1285  		return "", err
  1286  	}
  1287  
  1288  	yaml, err := yaml.Marshal(&config)
  1289  	if err != nil {
  1290  		return "", err
  1291  	}
  1292  
  1293  	return string(yaml), nil
  1294  }
  1295  
  1296  func (t *terminalService) getProjectIDbyClusterEdgeID(ctx context.Context, clusterEdgeID string) (string, error) {
  1297  	var projectID string
  1298  	row := t.SQLDB.QueryRowContext(ctx, sqlquery.GetProjectIDByClusterEdgeID, clusterEdgeID)
  1299  	if err := row.Scan(&projectID); err != nil {
  1300  		return "", err
  1301  	}
  1302  	return projectID, nil
  1303  }
  1304  
  1305  func (t *terminalService) getNodeResources(ctx context.Context, projectID string) ([]string, error) {
  1306  	// returns the terminal version from BQ and maps to the hostname
  1307  	logRequest := mapper.GetNodes()
  1308  	logRequest.GetClusterEdgeID = true
  1309  	return t.BQClient.GetKubeResource(ctx, projectID, nil, logRequest)
  1310  }
  1311  
  1312  func (t *terminalService) getTerminalVersionMapFromProjectID(ctx context.Context, projectID string) (map[string]map[string]string, error) {
  1313  	// get node resources from BQ then create a map of hostnames to node versions
  1314  	nodes, err := t.getNodeResources(ctx, projectID)
  1315  	if err != nil {
  1316  		return nil, err
  1317  	}
  1318  	return mapper.GenerateTerminalVersionMap(nodes)
  1319  }
  1320  
  1321  func (t *terminalService) updateTerminalVersions(ctx context.Context, terminals []*model.Terminal) ([]*model.Terminal, error) {
  1322  	if len(terminals) == 0 {
  1323  		return terminals, nil
  1324  	}
  1325  	projectID, err := t.getProjectIDbyClusterEdgeID(ctx, terminals[0].ClusterEdgeID)
  1326  	if err != nil {
  1327  		return nil, err
  1328  	}
  1329  	versionMap, err := t.getTerminalVersionMapFromProjectID(ctx, projectID)
  1330  
  1331  	if err != nil {
  1332  		return nil, err
  1333  	}
  1334  
  1335  	for _, terminal := range terminals {
  1336  		terminal.Version = versionMap[terminal.ClusterEdgeID][terminal.Hostname]
  1337  	}
  1338  	return terminals, nil
  1339  }
  1340  
  1341  func NewTerminalService(sqlDB *sql.DB, labelSvc LabelService) TerminalService {
  1342  	return &terminalService{
  1343  		SQLDB:        sqlDB,
  1344  		LabelService: labelSvc,
  1345  	}
  1346  }
  1347  
  1348  func NewTerminalServiceBQ(sqlDB *sql.DB, BQClient clients.BQClient, labelSvc LabelService) TerminalService {
  1349  	return &terminalService{
  1350  		SQLDB:        sqlDB,
  1351  		LabelService: labelSvc,
  1352  		BQClient:     BQClient,
  1353  	}
  1354  }
  1355  

View as plain text