Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ ARGS:
[additional-volumes.{index}] Additional local and block volumes attached to your server
[ip=new] Either an IP, an IP ID, 'new' to create a new IP, 'dynamic' to use a dynamic IP or 'none' for no public IP (new | dynamic | none | <id> | <address>)
[tags.{index}] Server tags
[ipv6] Enable IPv6
[ipv6] Enable IPv6, to be used with routed-ip-enabled=false
[stopped] Do not start server after its creation
[security-group-id] The security group ID used for this server
[placement-group-id] The placement group ID in which the server has to be created
Expand Down
2 changes: 1 addition & 1 deletion docs/commands/instance.md
Original file line number Diff line number Diff line change
Expand Up @@ -1712,7 +1712,7 @@ scw instance server create [arg=value ...]
| additional-volumes.{index} | | Additional local and block volumes attached to your server |
| ip | Default: `new` | Either an IP, an IP ID, 'new' to create a new IP, 'dynamic' to use a dynamic IP or 'none' for no public IP (new | dynamic | none | <id> | <address>) |
| tags.{index} | | Server tags |
| ipv6 | | Enable IPv6 |
| ipv6 | | Enable IPv6, to be used with routed-ip-enabled=false |
| stopped | | Do not start server after its creation |
| security-group-id | | The security group ID used for this server |
| placement-group-id | | The placement group ID in which the server has to be created |
Expand Down
264 changes: 42 additions & 222 deletions internal/namespaces/instance/v1/custom_server_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"context"
"fmt"
"net"
"reflect"
"strconv"
"strings"
Expand Down Expand Up @@ -99,7 +98,7 @@ func serverCreateCommand() *core.Command {
},
{
Name: "ipv6",
Short: "Enable IPv6",
Short: "Enable IPv6, to be used with routed-ip-enabled=false",
},
{
Name: "stopped",
Expand Down Expand Up @@ -193,245 +192,89 @@ func instanceWaitServerCreateRun() core.WaitFunc {
}

func instanceServerCreateRun(ctx context.Context, argsI interface{}) (i interface{}, e error) {
var err error
args := argsI.(*instanceCreateServerRequest)

//
// STEP 1: Argument validation and API requests creation.
// STEP 1: Argument handling and API requests creation.
//

needIPCreation := false

serverReq := &instance.CreateServerRequest{
Zone: args.Zone,
Organization: args.OrganizationID,
Project: args.ProjectID,
Name: args.Name,
CommercialType: args.Type,
EnableIPv6: scw.BoolPtr(args.IPv6),
Tags: args.Tags,
RoutedIPEnabled: args.RoutedIPEnabled,
AdminPasswordEncryptionSSHKeyID: args.AdminPasswordEncryptionSSHKeyID,
}

client := core.ExtractClient(ctx)
apiMarketplace := marketplace.NewAPI(client)
apiInstance := instance.NewAPI(client)

if commercialTypeIsWindowsServer(serverReq.CommercialType) && serverReq.AdminPasswordEncryptionSSHKeyID == nil {
return nil, &core.CliError{
Err: core.MissingRequiredArgumentError("admin-password-encryption-ssh-key-id").Err,
Details: "Expected a SSH Key ID to encrypt Admin RDP password. If not provided, no password will be generated. Key must be RSA Public Key.",
Hint: "Use completion or get your ssh key id using 'scw iam ssh-key list',",
Code: 1,
Empty: false,
}
}

//
// Image.
//
// Could be:
// - A local image UUID
// - An image label
//
switch {
case args.Image == "none":
break
case !validation.IsUUID(args.Image):
// For retro-compatibility, we replace dashes with underscores
imageLabel := strings.Replace(args.Image, "-", "_", -1)

// Find the corresponding local image UUID.
localImage, err := apiMarketplace.GetLocalImageByLabel(&marketplace.GetLocalImageByLabelRequest{
ImageLabel: imageLabel,
Zone: args.Zone,
CommercialType: serverReq.CommercialType,
Type: marketplace.LocalImageTypeInstanceLocal,
})
if err != nil {
return nil, err
}
serverReq.Image = localImage.ID
default:
serverReq.Image = args.Image
}

var (
getImageResponse *instance.GetImageResponse
serverType *instance.ServerType
)
if args.Image != "none" {
var err error
getImageResponse, err = apiInstance.GetImage(&instance.GetImageRequest{
Zone: args.Zone,
ImageID: serverReq.Image,
})
if err != nil {
logger.Warningf("cannot get image %s: %s", serverReq.Image, err)
}

serverType = getServerType(apiInstance, serverReq.Zone, serverReq.CommercialType)

if serverType != nil && getImageResponse != nil {
if err := validateImageServerTypeCompatibility(getImageResponse.Image, serverType, serverReq.CommercialType); err != nil {
return nil, err
}
} else {
logger.Warningf("skipping image server-type compatibility validation")
}
} else {
getImageResponse = nil
serverType = nil
serverBuilder := NewServerBuilder(client, args.Name, args.Zone, args.Type).
AddOrganizationID(args.OrganizationID).
AddProjectID(args.ProjectID).
AddEnableIPv6(scw.BoolPtr(args.IPv6)).
AddTags(args.Tags).
AddRoutedIPEnabled(args.RoutedIPEnabled).
AddAdminPasswordEncryptionSSHKeyID(args.AdminPasswordEncryptionSSHKeyID).
AddBootType(args.BootType).
AddSecurityGroup(args.SecurityGroupID).
AddPlacementGroup(args.PlacementGroupID)

serverBuilder, err = serverBuilder.AddImage(args.Image)
if err != nil {
return nil, err
}

//
// IP.
//
// Could be:
// - "new"
// - A flexible IP UUID
// - A flexible IP address
// - "dynamic"
// - "none"
//
switch {
case args.IP == "", args.IP == "new":
needIPCreation = true
case validation.IsUUID(args.IP):
serverReq.PublicIP = scw.StringPtr(args.IP)
case net.ParseIP(args.IP) != nil:
// Find the corresponding flexible IP UUID.
logger.Debugf("finding public IP UUID from address: %s", args.IP)
res, err := apiInstance.GetIP(&instance.GetIPRequest{
Zone: args.Zone,
IP: args.IP,
})
if err != nil { // FIXME: isNotFoundError
return nil, fmt.Errorf("%s does not belong to you", args.IP)
}
serverReq.PublicIP = scw.StringPtr(res.IP.ID)
case args.IP == "dynamic":
serverReq.DynamicIPRequired = scw.BoolPtr(true)
case args.IP == "none":
serverReq.DynamicIPRequired = scw.BoolPtr(false)
default:
return nil, fmt.Errorf(`invalid IP "%s", should be either 'new', 'dynamic', 'none', an IP address ID or a reserved flexible IP address`, args.IP)
serverBuilder, err = serverBuilder.AddIP(args.IP)
if err != nil {
return nil, err
}

//
// Volumes.
//
// More format details in buildVolumeTemplate function.
//
if len(args.AdditionalVolumes) > 0 || args.RootVolume != "" {
// Create initial volume template map.
volumes, err := buildVolumes(apiInstance, args.Zone, serverReq.Name, args.RootVolume, args.AdditionalVolumes)
if err != nil {
return nil, err
}

// Validate root volume type and size.
if args.Image != "none" && getImageResponse != nil {
if err := validateRootVolume(getImageResponse.Image.RootVolume.Size, volumes["0"]); err != nil {
return nil, err
}
} else {
logger.Warningf("skipping root volume validation")
}

// Validate total local volume sizes.
if args.Image != "none" && serverType != nil && getImageResponse != nil {
if err := validateLocalVolumeSizes(volumes, serverType, serverReq.CommercialType, getImageResponse.Image.RootVolume.Size); err != nil {
return nil, err
}
} else {
logger.Warningf("skip local volume size validation")
}

// Sanitize the volume map to respect API schemas
serverReq.Volumes = sanitizeVolumeMap(serverReq.Name, volumes)
serverBuilder, err = serverBuilder.AddVolumes(args.RootVolume, args.AdditionalVolumes)
if err != nil {
return nil, err
}

// Add default volumes to server, ex: scratch storage for GPU servers
if serverType != nil {
serverReq.Volumes = addDefaultVolumes(serverType, serverReq.Volumes)
serverBuilder, err = serverBuilder.AddBootscript(args.BootscriptID)
if err != nil {
return nil, err
}

//
// BootType.
// STEP 2: Validation and requests
//
bootType := instance.BootType(args.BootType)
serverReq.BootType = &bootType

//
// Bootscript.
//
if args.BootscriptID != "" {
if !validation.IsUUID(args.BootscriptID) {
return nil, fmt.Errorf("bootscript ID %s is not a valid UUID", args.BootscriptID)
}
//nolint: staticcheck // Bootscript is deprecated
_, err := apiInstance.GetBootscript(&instance.GetBootscriptRequest{
Zone: args.Zone,
BootscriptID: args.BootscriptID,
})
if err != nil { // FIXME: isNotFoundError
return nil, fmt.Errorf("bootscript ID %s does not exist", args.BootscriptID)
}

//nolint: staticcheck // Bootscript is deprecated
serverReq.Bootscript = scw.StringPtr(args.BootscriptID)
bootType := instance.BootTypeBootscript
serverReq.BootType = &bootType
}

//
// Security Group.
//
if args.SecurityGroupID != "" {
serverReq.SecurityGroup = scw.StringPtr(args.SecurityGroupID)
err = serverBuilder.Validate()
if err != nil {
return nil, err
}

//
// Placement Group.
//
if args.PlacementGroupID != "" {
serverReq.PlacementGroup = scw.StringPtr(args.PlacementGroupID)
}
createReq, createIPReq := serverBuilder.Build()
needIPCreation := createIPReq != nil

//
// STEP 2: Resource creations and modifications.
// IP creation
//
apiInstance := instance.NewAPI(client)

//
// IP
//
if needIPCreation {
logger.Debugf("creating IP")

ip, err := instanceServerCreateIPCreate(args, apiInstance)
ipRes, err := apiInstance.CreateIP(createIPReq)
if err != nil {
return nil, fmt.Errorf("error while creating your public IP: %s", err)
}
serverReq.PublicIP = scw.StringPtr(ip.ID)
logger.Debugf("IP created: %s", serverReq.PublicIP)
createReq.PublicIP = scw.StringPtr(ipRes.IP.ID)
logger.Debugf("IP created: %s", createReq.PublicIP)
}

//
// Server Creation
//
logger.Debugf("creating server")
serverRes, err := apiInstance.CreateServer(serverReq)
serverRes, err := apiInstance.CreateServer(createReq)
if err != nil {
if needIPCreation && serverReq.PublicIP != nil {
if needIPCreation && createReq.PublicIP != nil {
// Delete the created IP
logger.Debugf("deleting created IP: %s", serverReq.PublicIP)
logger.Debugf("deleting created IP: %s", createReq.PublicIP)
err := apiInstance.DeleteIP(&instance.DeleteIPRequest{
Zone: args.Zone,
IP: *serverReq.PublicIP,
IP: *createReq.PublicIP,
})
if err != nil {
logger.Warningf("cannot delete the create IP %s: %s.", serverReq.PublicIP, err)
logger.Warningf("cannot delete the create IP %s: %s.", createReq.PublicIP, err)
}
}

Expand Down Expand Up @@ -805,26 +648,3 @@ func getServerType(apiInstance *instance.API, zone scw.Zone, commercialType stri

return serverType
}

func instanceServerCreateIPCreate(args *instanceCreateServerRequest, api *instance.API) (*instance.IP, error) {
req := &instance.CreateIPRequest{
Zone: args.Zone,
Project: args.ProjectID,
Organization: args.OrganizationID,
}

if args.RoutedIPEnabled != nil {
if *args.RoutedIPEnabled {
req.Type = instance.IPTypeRoutedIPv4
} else {
req.Type = instance.IPTypeNat
}
}

res, err := api.CreateIP(req)
if err != nil {
return nil, err
}

return res.IP, nil
}
Loading