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
16 changes: 15 additions & 1 deletion v1/providers/verda/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const (
dockerFirewallDropInPath = "/etc/systemd/system/docker.service.d/10-brev-firewall.conf"
)

func buildStartupScript(rules v1.FirewallRules) (string, error) {
func buildStartupScript(rules v1.FirewallRules, publicKey string) (string, error) {
ufwRules, dockerRules, err := firewallRuleCommands(rules.IngressRules)
if err != nil {
return "", err
Expand All @@ -23,6 +23,20 @@ func buildStartupScript(rules v1.FirewallRules) (string, error) {
script.WriteString(`#!/bin/bash
set -u

if ! id brev >/dev/null 2>&1; then
useradd --create-home --shell /bin/bash brev
fi
install -d -m 0700 -o brev -g brev /home/brev/.ssh
cat > /home/brev/.ssh/authorized_keys <<'BREV_AUTHORIZED_KEYS'
`)
script.WriteString(publicKey)
script.WriteString(`
BREV_AUTHORIZED_KEYS
chown brev:brev /home/brev/.ssh/authorized_keys
chmod 0600 /home/brev/.ssh/authorized_keys
echo 'brev ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/brev
chmod 0440 /etc/sudoers.d/brev

if ! command -v ufw >/dev/null 2>&1; then
apt-get update -y
DEBIAN_FRONTEND=noninteractive apt-get install -y ufw iptables
Expand Down
20 changes: 16 additions & 4 deletions v1/providers/verda/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,8 @@ func TestGetInstanceTypesAndLocations(t *testing.T) { //nolint:funlen // One cat
assert.Equal(t, int32(1), h100.SupportedGPUs[0].Count)
assert.Equal(t, "H100", h100.SupportedGPUs[0].Name)
assert.Equal(t, "H100 80GB", h100.SupportedGPUs[0].Type)
assert.Equal(t, "NVMe", h100.SupportedStorage[0].Type)
assert.Equal(t, v1.Tebibyte, h100.SupportedStorage[0].SizeBytes.Unit())
assert.True(t, h100.Preemptible)
assert.Equal(t, "nvme", h100.SupportedStorage[0].Type)
assert.Equal(t, v1.Tebibyte, h100.SupportedStorage[0].MaxSizeBytes.Unit())
assert.True(t, h100.ElasticRootVolume)
assert.Equal(t, CloudProviderID, h100.Provider)
assert.Equal(t, []v1.Architecture{v1.ArchitectureX86_64}, h100.SupportedArchitectures)
Expand Down Expand Up @@ -297,6 +296,13 @@ func TestInstanceLifecycle(t *testing.T) { //nolint:gocyclo,funlen // One fixtur
writeJSON(t, w, createdInstance)
case r.Method == http.MethodGet && r.URL.Path == "/instances/instance-1":
writeJSON(t, w, createdInstance)
case r.Method == http.MethodGet && r.URL.Path == "/volumes/os-volume-1":
writeJSON(t, w, verdago.Volume{
ID: "os-volume-1",
Size: 100,
Type: verdago.VolumeTypeNVMe,
IsOSVolume: true,
})
case r.Method == http.MethodPut && r.URL.Path == "/instances":
require.NoError(t, json.NewDecoder(r.Body).Decode(&actionRequest))
w.WriteHeader(http.StatusAccepted)
Expand Down Expand Up @@ -333,15 +339,21 @@ func TestInstanceLifecycle(t *testing.T) { //nolint:gocyclo,funlen // One fixtur
assert.Equal(t, "ref-123", instance.RefID)
assert.Equal(t, "credential-ref", instance.CloudCredRefID)
assert.Equal(t, "ubuntu-24.04-cuda-12.8-open-docker", instance.ImageID)
assert.Equal(t, "brev", instance.SSHUser)
assert.Equal(t, v1.LifecycleStatusPending, instance.Status.LifecycleStatus)
assert.Equal(t, v1.InstanceTypeID("FIN-03-noSub-1H100.80S.22V"), instance.InstanceTypeID)
assert.Equal(t, "nvme", instance.VolumeType)
assert.Equal(t, v1.NewBytes(100, v1.Gibibyte), instance.DiskSizeBytes)
assertLegacyBytesMatch(t, instance.DiskSize, instance.DiskSizeBytes)

assert.Equal(t, "ref-123_credential-ref", createdRequest.Description)
assert.LessOrEqual(t, len(createdRequest.Description), maxInstanceDescriptionLength)
assert.Equal(t, "brev-key-ref-123", createdSSHKey.Name)
assert.Equal(t, authorizedKey, createdSSHKey.PublicKey)
assert.Equal(t, "brev-firewall-ref-123", createdScript.Name)
assert.Contains(t, createdScript.Script, "useradd --create-home --shell /bin/bash brev")
assert.Contains(t, createdScript.Script, authorizedKey)
assert.Contains(t, createdScript.Script, "brev ALL=(ALL) NOPASSWD:ALL")
assert.Equal(t, []string{"ssh-key-1"}, createdRequest.SSHKeyIDs)
require.NotNil(t, createdRequest.OSVolume)
assert.Equal(t, 100, createdRequest.OSVolume.Size)
Expand Down Expand Up @@ -419,7 +431,7 @@ func TestBuildStartupScriptRejectsUnsafeRules(t *testing.T) {
ToPort: 9999,
IPRanges: []string{"not-a-cidr"},
}},
})
}, "public-key")
require.Error(t, err)
}

Expand Down
52 changes: 34 additions & 18 deletions v1/providers/verda/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"strings"
"time"

"github.com/alecthomas/units"
v1 "github.com/brevdev/cloud/v1"
verdago "github.com/verda-cloud/verdacloud-sdk-go/pkg/verda"
"golang.org/x/crypto/ssh"
Expand All @@ -22,13 +23,13 @@ const (
instanceIdentitySeparator = "_"
firewallResourceNamePrefix = "brev-firewall"
sshKeyResourceNamePrefix = "brev-key"
defaultSSHUser = "root"
defaultSSHUser = "brev"
defaultSSHPort = 22
)

var resourceNameInvalidCharacters = regexp.MustCompile(`[^a-z0-9-]+`)

func (c *VerdaClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) { //nolint:gocyclo // complexity is acceptable for a single func
func (c *VerdaClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) { //nolint:gocyclo,funlen // complexity is acceptable for a single func
location := attrs.Location
if location == "" {
location = c.location
Expand Down Expand Up @@ -68,13 +69,17 @@ func (c *VerdaClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanc
}

// SSH keys are independent resources, so must be created and cleaned up separately
sshKeyID, err := c.ensureSSHKey(ctx, attrs.PublicKey, attrs.RefID)
publicKey, err := normalizeSSHPublicKey(attrs.PublicKey)
if err != nil {
return nil, err
}
sshKeyID, err := c.ensureSSHKey(ctx, publicKey, attrs.RefID)
if err != nil {
return nil, err
}

// Startup scripts are independent resources, so must be created and cleaned up separately
startupScript, err := buildStartupScript(attrs.FirewallRules)
startupScript, err := buildStartupScript(attrs.FirewallRules, publicKey)
if err != nil {
return nil, errors.Join(err, c.cleanupManagedResources(ctx, attrs.RefID))
}
Expand Down Expand Up @@ -110,15 +115,23 @@ func (c *VerdaClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanc
if err != nil {
return nil, errors.Join(wrapVerdaError(err), c.cleanupManagedResources(ctx, attrs.RefID))
}
return c.verdaInstanceToInstance(verdaInstance), nil
instance, err := c.verdaInstanceToInstance(ctx, verdaInstance)
if err != nil {
return nil, errors.Join(err, c.cleanupManagedResources(ctx, attrs.RefID))
}
return instance, nil
}

func (c *VerdaClient) GetInstance(ctx context.Context, id v1.CloudProviderInstanceID) (*v1.Instance, error) {
verdaInstance, err := c.client.Instances.GetByID(ctx, string(id))
if err != nil {
return nil, wrapVerdaError(err)
}
return c.verdaInstanceToInstance(verdaInstance), nil
instance, err := c.verdaInstanceToInstance(ctx, verdaInstance)
if err != nil {
return nil, err
}
return instance, nil
}

func (c *VerdaClient) ListInstances(ctx context.Context, args v1.ListInstancesArgs) ([]v1.Instance, error) {
Expand All @@ -129,7 +142,10 @@ func (c *VerdaClient) ListInstances(ctx context.Context, args v1.ListInstancesAr

instances := make([]v1.Instance, 0, len(verdaInstances))
for i := range verdaInstances {
instance := c.verdaInstanceToInstance(&verdaInstances[i])
instance, err := c.verdaInstanceToInstance(ctx, &verdaInstances[i])
if err != nil {
return nil, err
}
if len(args.InstanceIDs) > 0 && !slices.Contains(args.InstanceIDs, instance.CloudID) {
continue
}
Expand Down Expand Up @@ -223,11 +239,6 @@ func (c *VerdaClient) selectImage(ctx context.Context, instanceType string, requ
}

func (c *VerdaClient) ensureSSHKey(ctx context.Context, publicKey string, refID string) (string, error) {
publicKey, err := normalizeSSHPublicKey(publicKey)
if err != nil {
return "", err
}

keys, err := c.client.SSHKeys.GetAllSSHKeys(ctx)
if err != nil {
return "", wrapVerdaError(err)
Expand Down Expand Up @@ -311,7 +322,7 @@ func (c *VerdaClient) cleanupManagedResources(ctx context.Context, refID string)
return errors.Join(cleanupErrors...)
}

func (c *VerdaClient) verdaInstanceToInstance(verdaInstance *verdago.Instance) *v1.Instance {
func (c *VerdaClient) verdaInstanceToInstance(ctx context.Context, verdaInstance *verdago.Instance) (*v1.Instance, error) {
refID, cloudCredRefID := parseInstanceDescription(verdaInstance.Description)
if cloudCredRefID == "" {
cloudCredRefID = c.refID
Expand Down Expand Up @@ -345,12 +356,17 @@ func (c *VerdaClient) verdaInstanceToInstance(verdaInstance *verdago.Instance) *
}
instance.InstanceTypeID = v1.MakeGenericInstanceTypeIDFromInstance(*instance)

if storage := storageDescriptionToStorage(verdaInstance.Storage.Description); len(storage) > 0 {
instance.DiskSize = storage[0].Size
instance.DiskSizeBytes = storage[0].SizeBytes
instance.VolumeType = storage[0].Type
if verdaInstance.OSVolumeID != nil {
volume, err := c.client.Volumes.GetVolume(ctx, *verdaInstance.OSVolumeID)
if err != nil {
return nil, wrapVerdaError(err)
}
instance.VolumeType = "nvme"
instance.DiskSize = units.Base2Bytes(volume.Size) * units.GiB
instance.DiskSizeBytes = v1.NewBytes(v1.BytesValue(volume.Size), v1.Gibibyte)
}
return instance

return instance, nil
}

func verdaStatusToLifecycleStatus(status string) v1.LifecycleStatus {
Expand Down
86 changes: 24 additions & 62 deletions v1/providers/verda/instancetype.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ import (

const defaultCurrency = "usd"

var (
storageSizePattern = regexp.MustCompile(`(?i)(\d+)\s*(TiB|TB|GiB|GB|MiB|MB)`)
gpuMemorySuffixPattern = regexp.MustCompile(`(?i)\s+[0-9]+GB$`)
)
var gpuMemorySuffixPattern = regexp.MustCompile(`(?i)\s+[0-9]+GB$`)

func (c *VerdaClient) GetInstanceTypes(ctx context.Context, args v1.GetInstanceTypeArgs) ([]v1.InstanceType, error) {
verdaTypes, err := c.client.InstanceTypes.Get(ctx, defaultCurrency)
Expand Down Expand Up @@ -86,17 +83,14 @@ func verdaInstanceTypeToInstanceType(verdaType verdago.InstanceTypeInfo, locatio
MemoryBytes: memoryBytes,
VCPU: int32(verdaType.CPU.NumberOfCores), //nolint:gosec // ok
SupportedArchitectures: []v1.Architecture{verdaArchitecture(verdaType.Model)},
SupportedStorage: storageDescriptionToStorage(verdaType.Storage.Description),
SupportedStorage: buildSupportedStorage(),
ElasticRootVolume: true,
SupportedUsageClasses: usageClasses,
Stoppable: true,
Rebootable: false,
Preemptible: verdaType.SpotPrice > 0,
VariablePrice: verdaType.DynamicPrice > 0,
IsAvailable: true,
BasePrice: &basePrice,
Provider: CloudProviderID,
Cloud: CloudProviderID,
}

if verdaType.GPU.NumberOfGPUs > 0 {
Expand All @@ -117,6 +111,28 @@ func verdaInstanceTypeToInstanceType(verdaType verdago.InstanceTypeInfo, locatio
return instanceType, nil
}

func buildSupportedStorage() []v1.Storage {
minSize := 50 * units.GiB
minSizeBytes := v1.NewBytes(50, v1.Gibibyte)
maxSize := 10 * units.TiB
maxSizeBytes := v1.NewBytes(10, v1.Tebibyte)

pricePerGBHr, _ := currency.NewAmount("0.0002740", "USD")

@patelspratik patelspratik Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we move this somewhere more obvious for when we have to change it? Maybe constant at the top?


return []v1.Storage{
{
Type: "nvme",
Count: 1,
MinSize: &minSize,
MaxSize: &maxSize,
MinSizeBytes: &minSizeBytes,
MaxSizeBytes: &maxSizeBytes,
IsElastic: true,
PricePerGBHr: &pricePerGBHr,
},
}
}

func verdaGPUName(model string) string {
return gpuMemorySuffixPattern.ReplaceAllString(model, "")
}
Expand All @@ -137,60 +153,6 @@ func currencyCode(code string) string {
return "USD"
}

func storageDescriptionToStorage(description string) []v1.Storage {
match := storageSizePattern.FindStringSubmatch(description)
if len(match) != 3 {
return nil
}

size, err := strconv.ParseInt(match[1], 10, 64)
if err != nil {
return nil
}
byteUnit, ok := storageByteUnit(match[2])
if !ok {
return nil
}
legacySize, sizeBytes := byteSizes(size, byteUnit)

storageType := strings.TrimSpace(description)
upperDescription := strings.ToUpper(description)
switch {
case strings.Contains(upperDescription, "NVME"):
storageType = "NVMe"
case strings.Contains(upperDescription, "SSD"):
storageType = "SSD"
case strings.Contains(upperDescription, "HDD"):
storageType = "HDD"
}

return []v1.Storage{{
Count: 1,
Size: legacySize,
SizeBytes: sizeBytes,
Type: storageType,
}}
}

func storageByteUnit(unit string) (v1.BytesUnit, bool) {
switch strings.ToUpper(unit) {
case "MB":
return v1.Megabyte, true
case "MIB":
return v1.Mebibyte, true
case "GB":
return v1.Gigabyte, true
case "GIB":
return v1.Gibibyte, true
case "TB":
return v1.Terabyte, true
case "TIB":
return v1.Tebibyte, true
default:
return v1.BytesUnit{}, false
}
}

func byteSizes(value int64, unit v1.BytesUnit) (units.Base2Bytes, v1.Bytes) {
size := v1.NewBytes(v1.BytesValue(value), unit)
return units.Base2Bytes(size.ByteCount().Int64()), size
Expand Down
12 changes: 12 additions & 0 deletions v1/providers/verda/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ func TestValidationFunctions(t *testing.T) {
})
}

func TestGetInstance(t *testing.T) {
checkValidationCredentials(t)
credential := validationCredential()

client, err := credential.MakeClient(context.Background(), "")
require.NoError(t, err)

instance, err := client.GetInstance(context.Background(), v1.CloudProviderInstanceID("b395e9e7-9a21-4ff1-a4b5-fb06d9652942"))
require.NoError(t, err)
require.NotNil(t, instance)
}

func TestInstanceLifecycleValidation(t *testing.T) {
checkValidationCredentials(t)

Expand Down
Loading