package services import ( "context" "database/sql" "errors" "fmt" "strings" "github.com/google/uuid" sqlerr "edge-infra.dev/pkg/edge/api/apierror/sql" "edge-infra.dev/pkg/edge/api/graph/mapper" "edge-infra.dev/pkg/edge/api/graph/model" sqlquery "edge-infra.dev/pkg/edge/api/sql" "edge-infra.dev/pkg/edge/api/utils" ) type VirtualMachineService interface { CreateVirtualMachineEntry(ctx context.Context, createVM *model.VirtualMachineCreateInput) (*model.VirtualMachine, string, error) DeleteVirtualMachineEntry(ctx context.Context, virtualMachineID string) error UpdateVirtualMachineEntry(ctx context.Context, updateVM *model.VirtualMachineIDInput) (*model.VirtualMachine, error) GetVirtualMachine(ctx context.Context, virtualMachineID string) (*model.VirtualMachine, error) GetVirtualMachines(ctx context.Context, clusterEdgeID *string, virtualMachineHostname *string) ([]*model.VirtualMachine, error) CreateVirtualMachineDiskEntries(ctx context.Context, virtualMachineID string, createDisks []*model.VirtualMachineDiskCreateInput) ([]*model.VirtualMachineDisk, error) DeleteVirtualMachineDiskEntry(ctx context.Context, diskID string) (*model.VirtualMachineDisk, error) UpdateVirtualMachineDiskEntries(ctx context.Context, updateDisks []*model.VirtualMachineDiskIDInput) ([]*model.VirtualMachineDisk, error) GetVirtualMachineDisk(ctx context.Context, diskID string) (*model.VirtualMachineDisk, error) GetVirtualMachineDisks(ctx context.Context, virtualMachineID string) ([]*model.VirtualMachineDisk, error) GetVirtualMachineFromDisk(ctx context.Context, diskID string) (*model.VirtualMachine, error) CreateDSDSKubeVirtualMachineCR(virtualMachine *model.VirtualMachine) (string, error) GetVirtualMachineWithDisks(ctx context.Context, virtualMachineID string) (*model.VirtualMachine, error) } type virtualMachineService struct { SQLDB *sql.DB } func (v *virtualMachineService) CreateVirtualMachineEntry(ctx context.Context, createVM *model.VirtualMachineCreateInput) (virtualMachine *model.VirtualMachine, namespace string, err error) { if len(createVM.Disks) == 0 { return nil, "", fmt.Errorf("must provide at least one disk when creating a virtual machine") } transaction, err := v.SQLDB.BeginTx(ctx, nil) if err != nil { return nil, "", err } defer func() { if err != nil { err = errors.Join(err, transaction.Rollback()) } }() row := transaction.QueryRowContext(ctx, sqlquery.GetClusterNameByClusterEdgeIDQuery, createVM.ClusterEdgeID) var clusterName string if err = row.Scan(&clusterName); err != nil { return nil, "", err } namespaceService := NewNamespaceService(v.SQLDB) namespace, err = namespaceService.GetNamespaceName(ctx, createVM.NamespaceEdgeID) if err != nil { return nil, "", err } hostname := strings.ToLower(createVM.Hostname) createVM.Hostname = hostname modelVM := utils.CreateVirtualMachineModel(uuid.NewString(), namespace, createVM.ClusterEdgeID, clusterName, createVM.Hostname, *createVM.TargetPowerState, *createVM.Cpus, *createVM.Memory, *createVM.MachineType) virtualMachine = &modelVM // validate VirtualMachine if err = utils.ValidateVirtualMachine(virtualMachine); err != nil { return nil, "", err } if err = namespaceService.CreateClusterNamespaceEntry(ctx, transaction, createVM.NamespaceEdgeID, createVM.ClusterEdgeID); err != nil { return nil, "", err } clusterNamespaceEdgeID, err := namespaceService.GetClusterNamespaceEdgeID(ctx, transaction, createVM.NamespaceEdgeID, createVM.ClusterEdgeID) if err != nil { return nil, "", err } // check existing hostnames if err := v.checkHostnames(ctx, hostname, clusterNamespaceEdgeID); err != nil { return nil, "", err } args := []interface{}{ virtualMachine.VirtualMachineID, clusterNamespaceEdgeID, virtualMachine.Hostname, virtualMachine.TargetPowerState, virtualMachine.Cpus, virtualMachine.Memory, virtualMachine.MachineType, } if _, err = transaction.ExecContext(ctx, sqlquery.VirtualMachineCreateQuery, args...); err != nil { return nil, "", err } // we create provided disks in DB createdDisks, err := v.createVirtualMachineDiskEntries(ctx, transaction, createVM.Disks, virtualMachine.VirtualMachineID) if err != nil { return nil, "", err } if utils.HasDuplicateDiskBootOrders(createdDisks) { return nil, "", fmt.Errorf("cannot create vm with disks that have duplicate boot orders") } virtualMachine.Disks = createdDisks if err = transaction.Commit(); err != nil { return nil, "", err } return virtualMachine, namespace, nil } func (v *virtualMachineService) DeleteVirtualMachineEntry(ctx context.Context, virtualMachineID string) error { _, err := v.SQLDB.ExecContext(ctx, sqlquery.VirtualMachineDeleteQuery, virtualMachineID) return err } func (v *virtualMachineService) UpdateVirtualMachineEntry(ctx context.Context, updateVM *model.VirtualMachineIDInput) (virtualMachine *model.VirtualMachine, err error) { transaction, err := v.SQLDB.BeginTx(ctx, nil) if err != nil { return nil, err } defer func() { if err != nil { err = errors.Join(err, transaction.Rollback()) } }() // get the current values for the VirtualMachine in the db currentVM, err := v.GetVirtualMachine(ctx, updateVM.VirtualMachineID) if err != nil { return nil, err } // set current VirtualMachine values virtualMachine = utils.UpdateVirtualMachine(currentVM, updateVM) // validate the current VirtualMachine if err = utils.ValidateVirtualMachine(virtualMachine); err != nil { return nil, err } args := []interface{}{ virtualMachine.TargetPowerState, virtualMachine.Cpus, virtualMachine.Memory, virtualMachine.MachineType, virtualMachine.VirtualMachineID, } // update VirtualMachine in db if _, err = transaction.ExecContext(ctx, sqlquery.VirtualMachineUpdateQuery, args...); err != nil { return nil, err } // if any disks provided to update, we should update them in DB and set Disks attribute to new disk list if len(updateVM.VirtualMachineValues.Disks) != 0 { _, err := v.updateVirtualMachineDiskEntries(ctx, transaction, updateVM.VirtualMachineValues.Disks) if err != nil { return nil, err } } allDisks, err := v.getVirtualMachineDisks(ctx, transaction, updateVM.VirtualMachineID) if err != nil { return nil, err } if utils.HasDuplicateDiskBootOrders(allDisks) { return nil, fmt.Errorf("cannot update vm with disks that cause duplicate boot orders") } if err = transaction.Commit(); err != nil { return nil, err } virtualMachine.Disks = allDisks return virtualMachine, nil } func (v *virtualMachineService) GetVirtualMachine(ctx context.Context, virtualMachineID string) (*model.VirtualMachine, error) { row := v.SQLDB.QueryRowContext(ctx, sqlquery.GetVirtualMachineByIDQuery, virtualMachineID) virtualMachine, err := v.scanVirtualMachineRow(row) if err != nil { return nil, err } return virtualMachine, nil } func (v *virtualMachineService) GetVirtualMachines(ctx context.Context, clusterEdgeID *string, virtualMachineHostname *string) ([]*model.VirtualMachine, error) { switch { case clusterEdgeID == nil && virtualMachineHostname == nil: return v.getAllVirtualMachines(ctx) case clusterEdgeID != nil && virtualMachineHostname == nil: return v.getClusterEdgeIDVirtualMachines(ctx, *clusterEdgeID) case clusterEdgeID == nil && virtualMachineHostname != nil: return v.getHostnameVirtualMachines(ctx, *virtualMachineHostname) default: return v.getClusterEdgeIDAndHostnameVirtualMachines(ctx, *clusterEdgeID, *virtualMachineHostname) } } func (v *virtualMachineService) CreateDSDSKubeVirtualMachineCR(virtualMachine *model.VirtualMachine) (string, error) { kubeVM, err := mapper.VirtualMachineToKubeVirtualMachine(virtualMachine) if err != nil { return "", err } encodedKubeVM, err := utils.ConvertStructToBase64(kubeVM) if err != nil { return "", err } return encodedKubeVM, nil } func (v *virtualMachineService) GetVirtualMachineWithDisks(ctx context.Context, virtualMachineID string) (*model.VirtualMachine, error) { virtualMachine, err := v.GetVirtualMachine(ctx, virtualMachineID) if err != nil { return nil, err } virtualMachine.Disks, err = v.GetVirtualMachineDisks(ctx, virtualMachineID) if err != nil { return nil, err } return virtualMachine, nil } func (v *virtualMachineService) scanVirtualMachineRow(row *sql.Row) (*model.VirtualMachine, error) { virtualMachine := &model.VirtualMachine{} err := row.Scan(&virtualMachine.VirtualMachineID, &virtualMachine.Namespace, &virtualMachine.ClusterEdgeID, &virtualMachine.ClusterName, &virtualMachine.Hostname, &virtualMachine.TargetPowerState, &virtualMachine.Cpus, &virtualMachine.Memory, &virtualMachine.MachineType) if err != nil { return nil, err } return virtualMachine, nil } func (v *virtualMachineService) scanVirtualMachineRows(rows *sql.Rows) ([]*model.VirtualMachine, error) { virtualMachines := []*model.VirtualMachine{} for rows.Next() { virtualMachine := &model.VirtualMachine{} err := rows.Scan(&virtualMachine.VirtualMachineID, &virtualMachine.Namespace, &virtualMachine.ClusterEdgeID, &virtualMachine.ClusterName, &virtualMachine.Hostname, &virtualMachine.TargetPowerState, &virtualMachine.Cpus, &virtualMachine.Memory, &virtualMachine.MachineType) if err != nil { return nil, err } virtualMachines = append(virtualMachines, virtualMachine) } if err := rows.Err(); err != nil { return nil, sqlerr.Wrap(err) } return virtualMachines, nil } func (v *virtualMachineService) getAllVirtualMachines(ctx context.Context) ([]*model.VirtualMachine, error) { rows, err := v.SQLDB.QueryContext(ctx, sqlquery.GetAllVirtualMachinesQuery) if err != nil { return nil, err } virtualMachines, err := v.scanVirtualMachineRows(rows) if err != nil { return nil, err } return virtualMachines, nil } func (v *virtualMachineService) getClusterEdgeIDVirtualMachines(ctx context.Context, clusterEdgeID string) ([]*model.VirtualMachine, error) { rows, err := v.SQLDB.QueryContext(ctx, sqlquery.GetVirtualMachinesByClusterEdgeIDQuery, clusterEdgeID) if err != nil { return nil, err } virtualMachines, err := v.scanVirtualMachineRows(rows) if err != nil { return nil, err } return virtualMachines, nil } func (v *virtualMachineService) getHostnameVirtualMachines(ctx context.Context, virtualMachineHostname string) ([]*model.VirtualMachine, error) { rows, err := v.SQLDB.QueryContext(ctx, sqlquery.GetVirtualMachinesByHostnameQuery, virtualMachineHostname) if err != nil { return nil, err } virtualMachines, err := v.scanVirtualMachineRows(rows) if err != nil { return nil, err } return virtualMachines, nil } func (v *virtualMachineService) getClusterEdgeIDAndHostnameVirtualMachines(ctx context.Context, clusterEdgeID, virtualMachineHostname string) ([]*model.VirtualMachine, error) { rows, err := v.SQLDB.QueryContext(ctx, sqlquery.GetVirtualMachinesByClusterEdgeIDAndHostnameQuery, clusterEdgeID, virtualMachineHostname) if err != nil { return nil, err } virtualMachines, err := v.scanVirtualMachineRows(rows) if err != nil { return nil, err } return virtualMachines, nil } func (v *virtualMachineService) checkHostnames(ctx context.Context, nodeName string, clusterNamespaceEdgeID string) error { // get all of the hostnames in the cluster namespace rows, err := v.SQLDB.QueryContext(ctx, sqlquery.GetVirtualMachineHostnamesForAClusterNamespaceQuery, clusterNamespaceEdgeID) if err != nil { return err } var hostname string for rows.Next() { err := rows.Scan(&hostname) if err != nil { return err } if hostname == nodeName { return fmt.Errorf("cannot create VirtualMachine - hostname %s already exists in this cluster namespace", hostname) } } if err := rows.Err(); err != nil { return sqlerr.Wrap(err) } return nil } func NewVirtualMachineService(sqlDB *sql.DB) *virtualMachineService { //nolint return &virtualMachineService{ SQLDB: sqlDB, } }