diff --git a/cmd/api/api/volumes.go b/cmd/api/api/volumes.go index 0e450da6..c2471e9e 100644 --- a/cmd/api/api/volumes.go +++ b/cmd/api/api/volumes.go @@ -46,11 +46,31 @@ func (s *ApiService) CreateVolume(ctx context.Context, request oapi.CreateVolume }, nil } + var accessMode volumes.AccessMode + if request.Body.AccessMode != nil { + accessMode = volumes.AccessMode(*request.Body.AccessMode) + } + var nfsConfig *volumes.NFSConfig + if request.Body.Nfs != nil { + nfsConfig = &volumes.NFSConfig{ + Server: request.Body.Nfs.Server, + ExportPath: request.Body.Nfs.ExportPath, + } + if request.Body.Nfs.Version != nil { + nfsConfig.Version = *request.Body.Nfs.Version + } + if request.Body.Nfs.Options != nil { + nfsConfig.Options = *request.Body.Nfs.Options + } + } + domainReq := volumes.CreateVolumeRequest{ - Name: request.Body.Name, - SizeGb: request.Body.SizeGb, - Id: request.Body.Id, - Tags: toMapTags(request.Body.Tags), + Name: request.Body.Name, + SizeGb: request.Body.SizeGb, + AccessMode: accessMode, + NFS: nfsConfig, + Id: request.Body.Id, + Tags: toMapTags(request.Body.Tags), } vol, err := s.VolumeManager.CreateVolume(ctx, domainReq) @@ -61,7 +81,7 @@ func (s *ApiService) CreateVolume(ctx context.Context, request oapi.CreateVolume Message: "volume with this ID already exists", }, nil } - if errors.Is(err, tags.ErrInvalidTags) { + if errors.Is(err, tags.ErrInvalidTags) || errors.Is(err, volumes.ErrInvalidRequest) { return oapi.CreateVolume400JSONResponse{ Code: "invalid_request", Message: err.Error(), @@ -182,12 +202,28 @@ func (s *ApiService) DeleteVolume(ctx context.Context, request oapi.DeleteVolume } func volumeToOAPI(vol volumes.Volume) oapi.Volume { + accessMode := oapi.VolumeAccessMode(vol.AccessMode) oapiVol := oapi.Volume{ - Id: vol.Id, - Name: vol.Name, - SizeGb: vol.SizeGb, - Tags: toOAPITags(vol.Tags), - CreatedAt: vol.CreatedAt, + Id: vol.Id, + Name: vol.Name, + SizeGb: vol.SizeGb, + AccessMode: &accessMode, + Tags: toOAPITags(vol.Tags), + CreatedAt: vol.CreatedAt, + } + + if vol.NFS != nil { + nfs := &oapi.NFSConfig{ + Server: vol.NFS.Server, + ExportPath: vol.NFS.ExportPath, + } + if vol.NFS.Version != "" { + nfs.Version = &vol.NFS.Version + } + if vol.NFS.Options != "" { + nfs.Options = &vol.NFS.Options + } + oapiVol.Nfs = nfs } // Convert attachments diff --git a/lib/instances/admission.go b/lib/instances/admission.go index cc22a099..b2832d46 100644 --- a/lib/instances/admission.go +++ b/lib/instances/admission.go @@ -3,7 +3,7 @@ package instances func volumeOverlayReservationBytes(volumes []VolumeAttachment) int64 { var total int64 for _, vol := range volumes { - if vol.Overlay { + if vol.Overlay && vol.NFS == nil { total += vol.OverlaySize } } diff --git a/lib/instances/configdisk.go b/lib/instances/configdisk.go index f85fa7af..34cf6cf6 100644 --- a/lib/instances/configdisk.go +++ b/lib/instances/configdisk.go @@ -98,9 +98,28 @@ func (m *manager) buildGuestConfig(ctx context.Context, inst *Instance, imageInf } // Volume mounts - // Volumes are attached as /dev/vdd, /dev/vde, etc. (after vda=rootfs, vdb=overlay, vdc=config) + // Block volumes are attached as /dev/vdd, /dev/vde, etc. (after vda=rootfs, vdb=overlay, vdc=config) + // NFS volumes don't consume a block device — they're mounted via the network. deviceIdx := 0 for _, vol := range inst.Volumes { + if vol.NFS != nil { + // NFS-backed volume (ReadWriteMany) — no block device, guest mounts via NFS + mount := vmconfig.VolumeMount{ + Path: vol.MountPath, + Mode: "nfs", + NFSServer: vol.NFS.Server, + NFSExportPath: vol.NFS.ExportPath, + NFSVersion: vol.NFS.Version, + NFSOptions: vol.NFS.Options, + } + if vol.Readonly { + mount.Mode = "nfs_ro" + } + cfg.VolumeMounts = append(cfg.VolumeMounts, mount) + continue + } + + // Block-backed volume device := fmt.Sprintf("/dev/vd%c", 'd'+deviceIdx) mount := vmconfig.VolumeMount{ Device: device, diff --git a/lib/instances/create.go b/lib/instances/create.go index c6d208ac..92136514 100644 --- a/lib/instances/create.go +++ b/lib/instances/create.go @@ -420,14 +420,31 @@ func (m *manager) createInstance( // 15. Validate and attach volumes if len(req.Volumes) > 0 { log.DebugContext(ctx, "validating volumes", "instance_id", id, "count", len(req.Volumes)) - for _, volAttach := range req.Volumes { - // Check volume exists - _, err := m.volumeManager.GetVolume(ctx, volAttach.VolumeID) + resolvedVolumes := make([]VolumeAttachment, len(req.Volumes)) + for i, volAttach := range req.Volumes { + // Check volume exists and get its details + vol, err := m.volumeManager.GetVolume(ctx, volAttach.VolumeID) if err != nil { log.ErrorContext(ctx, "volume not found", "instance_id", id, "volume_id", volAttach.VolumeID, "error", err) return nil, fmt.Errorf("volume %s: %w", volAttach.VolumeID, err) } + resolvedVolumes[i] = volAttach + + // Populate NFS config from the volume for RWX volumes + if vol.NFS != nil { + resolvedVolumes[i].NFS = &VolumeNFSConfig{ + Server: vol.NFS.Server, + ExportPath: vol.NFS.ExportPath, + Version: vol.NFS.Version, + Options: vol.NFS.Options, + } + // NFS volumes require networking for NFS client access + if !req.NetworkEnabled { + return nil, fmt.Errorf("volume %s: NFS volumes require network.enabled=true", volAttach.VolumeID) + } + } + // Mark volume as attached (AttachVolume handles multi-attach validation) if err := m.volumeManager.AttachVolume(ctx, volAttach.VolumeID, volumes.AttachVolumeRequest{ InstanceID: id, @@ -444,7 +461,7 @@ func (m *manager) createInstance( m.volumeManager.DetachVolume(ctx, volumeID, id) }) - // Create overlay disk for volumes with overlay enabled + // Create overlay disk for volumes with overlay enabled (not for NFS) if volAttach.Overlay { log.DebugContext(ctx, "creating volume overlay disk", "instance_id", id, "volume_id", volAttach.VolumeID, "size", volAttach.OverlaySize) if err := m.createVolumeOverlayDisk(id, volAttach.VolumeID, volAttach.OverlaySize); err != nil { @@ -453,8 +470,12 @@ func (m *manager) createInstance( } } } - // Store volume attachments in metadata - stored.Volumes = req.Volumes + // Re-validate with NFS info populated + if err := validateVolumeAttachments(resolvedVolumes); err != nil { + return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err) + } + // Store resolved volume attachments in metadata + stored.Volumes = resolvedVolumes } // 16. Create config disk (needs Instance for buildVMConfig) @@ -614,10 +635,13 @@ func validateCreateRequest(req *CreateInstanceRequest) error { } // validateVolumeAttachments validates volume attachment requests -func validateVolumeAttachments(volumes []VolumeAttachment) error { - // Count total devices needed (each overlay volume needs 2 devices: base + overlay) +func validateVolumeAttachments(vols []VolumeAttachment) error { + // Count total block devices needed (NFS volumes don't consume a device) totalDevices := 0 - for _, vol := range volumes { + for _, vol := range vols { + if vol.NFS != nil { + continue // NFS volumes don't use block devices + } totalDevices++ if vol.Overlay { totalDevices++ // Overlay needs an additional device @@ -628,7 +652,7 @@ func validateVolumeAttachments(volumes []VolumeAttachment) error { } seenPaths := make(map[string]bool) - for _, vol := range volumes { + for _, vol := range vols { // Validate mount path is absolute if !filepath.IsAbs(vol.MountPath) { return fmt.Errorf("volume %s: mount path %q must be absolute", vol.VolumeID, vol.MountPath) @@ -648,6 +672,11 @@ func validateVolumeAttachments(volumes []VolumeAttachment) error { } seenPaths[cleanPath] = true + // NFS volumes cannot use overlay mode + if vol.NFS != nil && vol.Overlay { + return fmt.Errorf("volume %s: overlay mode is not supported for NFS volumes", vol.VolumeID) + } + // Validate overlay mode requirements if vol.Overlay { if !vol.Readonly { @@ -765,8 +794,11 @@ func (m *manager) buildHypervisorConfig(ctx context.Context, inst *Instance, ima {Path: m.paths.InstanceConfigDisk(inst.Id), Readonly: true, IOBps: ioBps, IOBurstBps: burstBps}, } - // Add attached volumes as additional disks + // Add attached volumes as additional disks (skip NFS volumes — they're network-mounted) for _, volAttach := range inst.Volumes { + if volAttach.NFS != nil { + continue // NFS volumes don't have a local block device + } volumePath := m.volumeManager.GetVolumePath(volAttach.VolumeID) if volAttach.Overlay { // Base volume is always read-only when overlay is enabled diff --git a/lib/instances/types.go b/lib/instances/types.go index 141a6b9d..823d1168 100644 --- a/lib/instances/types.go +++ b/lib/instances/types.go @@ -30,13 +30,22 @@ const ( EgressEnforcementModeHTTPHTTPSOnly EgressEnforcementMode = "http_https_only" ) +// VolumeNFSConfig holds NFS connection details for an NFS-backed volume attachment. +type VolumeNFSConfig struct { + Server string // NFS server hostname or IP + ExportPath string // NFS export path + Version string // NFS protocol version (e.g., "4.1") + Options string // Additional mount options +} + // VolumeAttachment represents a volume attached to an instance type VolumeAttachment struct { - VolumeID string // Volume ID - MountPath string // Mount path in guest - Readonly bool // Whether mounted read-only - Overlay bool // If true, create per-instance overlay for writes (requires Readonly=true) - OverlaySize int64 // Size of overlay disk in bytes (max diff from base) + VolumeID string // Volume ID + MountPath string // Mount path in guest + Readonly bool // Whether mounted read-only + Overlay bool // If true, create per-instance overlay for writes (requires Readonly=true) + OverlaySize int64 // Size of overlay disk in bytes (max diff from base) + NFS *VolumeNFSConfig // NFS config (set for ReadWriteMany volumes, nil for block volumes) } // NetworkEgressPolicy configures host-mediated outbound networking behavior. diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index 8ceb515c..b33333eb 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -87,6 +87,13 @@ const ( HttpHttpsOnly CreateInstanceRequestNetworkEgressEnforcementMode = "http_https_only" ) +// Defines values for CreateVolumeRequestAccessMode. +const ( + CreateVolumeRequestAccessModeReadOnlyMany CreateVolumeRequestAccessMode = "ReadOnlyMany" + CreateVolumeRequestAccessModeReadWriteMany CreateVolumeRequestAccessMode = "ReadWriteMany" + CreateVolumeRequestAccessModeReadWriteOnce CreateVolumeRequestAccessMode = "ReadWriteOnce" +) + // Defines values for DeviceType. const ( Gpu DeviceType = "gpu" @@ -205,6 +212,13 @@ const ( SnapshotTargetStateStopped SnapshotTargetState = "Stopped" ) +// Defines values for VolumeAccessMode. +const ( + VolumeAccessModeReadOnlyMany VolumeAccessMode = "ReadOnlyMany" + VolumeAccessModeReadWriteMany VolumeAccessMode = "ReadWriteMany" + VolumeAccessModeReadWriteOnce VolumeAccessMode = "ReadWriteOnce" +) + // Defines values for GetInstanceLogsParamsSource. const ( App GetInstanceLogsParamsSource = "app" @@ -583,12 +597,21 @@ type CreateSnapshotRequest struct { // CreateVolumeRequest defines model for CreateVolumeRequest. type CreateVolumeRequest struct { + // AccessMode Volume access mode. Determines multi-attach behavior: + // - ReadWriteOnce (default): single read-write or multiple read-only attachments + // - ReadOnlyMany: multiple read-only attachments only + // - ReadWriteMany: multiple read-write attachments (requires NFS config) + AccessMode *CreateVolumeRequestAccessMode `json:"access_mode,omitempty"` + // Id Optional custom identifier (auto-generated if not provided) Id *string `json:"id,omitempty"` // Name Volume name Name string `json:"name"` + // Nfs NFS server connection details for ReadWriteMany volumes. + Nfs *NFSConfig `json:"nfs,omitempty"` + // SizeGb Size in gigabytes SizeGb int `json:"size_gb"` @@ -596,6 +619,12 @@ type CreateVolumeRequest struct { Tags *Tags `json:"tags,omitempty"` } +// CreateVolumeRequestAccessMode Volume access mode. Determines multi-attach behavior: +// - ReadWriteOnce (default): single read-write or multiple read-only attachments +// - ReadOnlyMany: multiple read-only attachments only +// - ReadWriteMany: multiple read-write attachments (requires NFS config) +type CreateVolumeRequestAccessMode string + // Device defines model for Device. type Device struct { // AttachedTo Instance ID if attached @@ -1063,6 +1092,21 @@ type MemoryReclaimResponse struct { // MemoryReclaimResponseHostPressureState defines model for MemoryReclaimResponse.HostPressureState. type MemoryReclaimResponseHostPressureState string +// NFSConfig NFS server connection details for ReadWriteMany volumes. +type NFSConfig struct { + // ExportPath NFS export path + ExportPath string `json:"export_path"` + + // Options Additional NFS mount options + Options *string `json:"options,omitempty"` + + // Server NFS server hostname or IP address + Server string `json:"server"` + + // Version NFS protocol version (defaults to "4.1") + Version *string `json:"version,omitempty"` +} + // PassthroughDevice Physical GPU available for passthrough type PassthroughDevice struct { // Available Whether this GPU is available (not attached to an instance) @@ -1335,6 +1379,9 @@ type UpdateInstanceRequest struct { // Volume defines model for Volume. type Volume struct { + // AccessMode Volume access mode + AccessMode *VolumeAccessMode `json:"access_mode,omitempty"` + // Attachments List of current attachments (empty if not attached) Attachments *[]VolumeAttachment `json:"attachments,omitempty"` @@ -1347,6 +1394,9 @@ type Volume struct { // Name Volume name Name string `json:"name"` + // Nfs NFS server connection details for ReadWriteMany volumes. + Nfs *NFSConfig `json:"nfs,omitempty"` + // SizeGb Size in gigabytes SizeGb int `json:"size_gb"` @@ -1354,6 +1404,9 @@ type Volume struct { Tags *Tags `json:"tags,omitempty"` } +// VolumeAccessMode Volume access mode +type VolumeAccessMode string + // VolumeAttachment defines model for VolumeAttachment. type VolumeAttachment struct { // InstanceId ID of the instance this volume is attached to @@ -15648,272 +15701,276 @@ func (sh *strictHandler) GetVolume(w http.ResponseWriter, r *http.Request, id st // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "yWFyAQA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/lib/vmconfig/config.go b/lib/vmconfig/config.go index 9d073077..bb2391a6 100644 --- a/lib/vmconfig/config.go +++ b/lib/vmconfig/config.go @@ -38,8 +38,14 @@ type Config struct { type VolumeMount struct { Device string `json:"device"` Path string `json:"path"` - Mode string `json:"mode"` // "ro", "rw", or "overlay" + Mode string `json:"mode"` // "ro", "rw", "overlay", or "nfs" OverlayDevice string `json:"overlay_device,omitempty"` + + // NFS-specific fields (only used when Mode is "nfs") + NFSServer string `json:"nfs_server,omitempty"` + NFSExportPath string `json:"nfs_export_path,omitempty"` + NFSVersion string `json:"nfs_version,omitempty"` + NFSOptions string `json:"nfs_options,omitempty"` } // EgressProxyConfig configures guest-side trust and proxy endpoint wiring. diff --git a/lib/volumes/errors.go b/lib/volumes/errors.go index 522ceda9..a5c00b1d 100644 --- a/lib/volumes/errors.go +++ b/lib/volumes/errors.go @@ -3,8 +3,9 @@ package volumes import "errors" var ( - ErrNotFound = errors.New("volume not found") - ErrInUse = errors.New("volume is in use") - ErrAlreadyExists = errors.New("volume already exists") - ErrAmbiguousName = errors.New("multiple volumes with the same name") + ErrNotFound = errors.New("volume not found") + ErrInUse = errors.New("volume is in use") + ErrAlreadyExists = errors.New("volume already exists") + ErrAmbiguousName = errors.New("multiple volumes with the same name") + ErrInvalidRequest = errors.New("invalid volume request") ) diff --git a/lib/volumes/manager.go b/lib/volumes/manager.go index f33d0b86..acf7e7bf 100644 --- a/lib/volumes/manager.go +++ b/lib/volumes/manager.go @@ -25,18 +25,23 @@ type Manager interface { DeleteVolume(ctx context.Context, id string) error // Attachment operations (called by instance manager) - // Multi-attach rules: - // - If no attachments: allow any mode (rw or ro) - // - If existing attachment is rw: reject all new attachments - // - If existing attachments are ro: only allow new ro attachments + // Multi-attach rules depend on access mode: + // - ReadWriteOnce (default): single rw attachment; multiple ro allowed + // - ReadOnlyMany: multiple ro attachments only + // - ReadWriteMany (NFS): multiple rw or ro attachments allowed AttachVolume(ctx context.Context, id string, req AttachVolumeRequest) error DetachVolume(ctx context.Context, volumeID string, instanceID string) error - // GetVolumePath returns the path to the volume data file + // GetVolumePath returns the path to the volume data file. + // Returns empty string for NFS-backed volumes (no local data file). GetVolumePath(id string) string + // IsNFSVolume returns true if the volume uses NFS backing. + IsNFSVolume(ctx context.Context, id string) bool + // TotalVolumeBytes returns the total size of all volumes. // Used by the resource manager for disk capacity tracking. + // NFS volumes are excluded from the total (they don't consume local disk). TotalVolumeBytes(ctx context.Context) (int64, error) } @@ -97,7 +102,8 @@ func (m *manager) ListVolumes(ctx context.Context) ([]Volume, error) { return volumes, nil } -// calculateTotalVolumeStorage calculates total storage used by all volumes +// calculateTotalVolumeStorage calculates total storage used by all volumes. +// NFS volumes are excluded (they don't consume local disk). func (m *manager) calculateTotalVolumeStorage(ctx context.Context) (int64, error) { volumes, err := m.ListVolumes(ctx) if err != nil { @@ -106,6 +112,9 @@ func (m *manager) calculateTotalVolumeStorage(ctx context.Context) (int64, error var totalBytes int64 for _, vol := range volumes { + if vol.AccessMode == AccessModeReadWriteMany { + continue // NFS volumes don't use local disk + } totalBytes += int64(vol.SizeGb) * 1024 * 1024 * 1024 } return totalBytes, nil @@ -161,6 +170,30 @@ func (m *manager) CreateVolume(ctx context.Context, req CreateVolumeRequest) (*V return nil, err } + // Default access mode + accessMode := req.AccessMode + if accessMode == "" { + accessMode = AccessModeReadWriteOnce + } + if !ValidAccessMode(accessMode) { + return nil, fmt.Errorf("%w: invalid access_mode %q", ErrInvalidRequest, accessMode) + } + + // Validate NFS configuration + if accessMode == AccessModeReadWriteMany { + if req.NFS == nil { + return nil, fmt.Errorf("%w: nfs configuration is required for ReadWriteMany volumes", ErrInvalidRequest) + } + if req.NFS.Server == "" { + return nil, fmt.Errorf("%w: nfs.server is required", ErrInvalidRequest) + } + if req.NFS.ExportPath == "" { + return nil, fmt.Errorf("%w: nfs.export_path is required", ErrInvalidRequest) + } + } else if req.NFS != nil { + return nil, fmt.Errorf("%w: nfs configuration is only valid for ReadWriteMany volumes", ErrInvalidRequest) + } + // Generate or use provided ID id := cuid2.Generate() if req.Id != nil && *req.Id != "" { @@ -172,8 +205,10 @@ func (m *manager) CreateVolume(ctx context.Context, req CreateVolumeRequest) (*V return nil, ErrAlreadyExists } - // Check total volume storage limit - if m.maxTotalVolumeStorage > 0 { + isNFS := accessMode == AccessModeReadWriteMany + + // Check total volume storage limit (NFS volumes don't consume local disk) + if !isNFS && m.maxTotalVolumeStorage > 0 { currentStorage, err := m.getTotalVolumeBytes(ctx) if err != nil { // Log but don't fail - continue with creation @@ -191,31 +226,46 @@ func (m *manager) CreateVolume(ctx context.Context, req CreateVolumeRequest) (*V return nil, err } - // Create and format the disk - if err := createVolumeDisk(m.paths, id, req.SizeGb); err != nil { - // Cleanup on error - deleteVolumeData(m.paths, id) - return nil, err + // Create and format the disk (skip for NFS volumes — no local data file) + if !isNFS { + if err := createVolumeDisk(m.paths, id, req.SizeGb); err != nil { + deleteVolumeData(m.paths, id) + return nil, err + } } // Create metadata now := time.Now() meta := &storedMetadata{ - Id: id, - Name: req.Name, - SizeGb: req.SizeGb, - Tags: tags.Clone(req.Tags), - CreatedAt: now.Format(time.RFC3339), + Id: id, + Name: req.Name, + SizeGb: req.SizeGb, + AccessMode: accessMode, + Tags: tags.Clone(req.Tags), + CreatedAt: now.Format(time.RFC3339), + } + if isNFS { + nfsVersion := req.NFS.Version + if nfsVersion == "" { + nfsVersion = "4.1" + } + meta.NFS = &storedNFSConfig{ + Server: req.NFS.Server, + ExportPath: req.NFS.ExportPath, + Version: nfsVersion, + Options: req.NFS.Options, + } } // Save metadata if err := saveMetadata(m.paths, meta); err != nil { - // Cleanup on error deleteVolumeData(m.paths, id) return nil, err } - m.addVolumeBytes(int64(req.SizeGb) * 1024 * 1024 * 1024) + if !isNFS { + m.addVolumeBytes(int64(req.SizeGb) * 1024 * 1024 * 1024) + } m.recordCreateDuration(ctx, start, "success") return m.metadataToVolume(meta), nil @@ -369,7 +419,10 @@ func (m *manager) DeleteVolume(ctx context.Context, id string) error { return err } - m.subtractVolumeBytes(int64(meta.SizeGb) * 1024 * 1024 * 1024) + // Only adjust local disk tracking for non-NFS volumes + if meta.AccessMode != AccessModeReadWriteMany { + m.subtractVolumeBytes(int64(meta.SizeGb) * 1024 * 1024 * 1024) + } // Clean up lock m.volumeLocks.Delete(id) @@ -377,11 +430,11 @@ func (m *manager) DeleteVolume(ctx context.Context, id string) error { return nil } -// AttachVolume marks a volume as attached to an instance -// Multi-attach rules (dynamic based on current state): -// - If no attachments: allow any mode (rw or ro) -// - If existing attachment is rw: reject all new attachments -// - If existing attachments are ro: only allow new ro attachments +// AttachVolume marks a volume as attached to an instance. +// Multi-attach rules depend on the volume's access mode: +// - ReadWriteOnce: single rw or multiple ro (existing behavior) +// - ReadOnlyMany: multiple ro only; rw rejected +// - ReadWriteMany (NFS): any number of rw or ro attachments func (m *manager) AttachVolume(ctx context.Context, id string, req AttachVolumeRequest) error { lock := m.getVolumeLock(id) lock.Lock() @@ -399,17 +452,30 @@ func (m *manager) AttachVolume(ctx context.Context, id string, req AttachVolumeR } } - // Apply multi-attach rules - if len(meta.Attachments) > 0 { - // Check if any existing attachment is read-write - for _, att := range meta.Attachments { - if !att.Readonly { - return fmt.Errorf("volume has exclusive read-write attachment to instance %s", att.InstanceID) - } - } - // Existing attachments are all read-only, new attachment must also be read-only + accessMode := meta.AccessMode + if accessMode == "" { + accessMode = AccessModeReadWriteOnce + } + + switch accessMode { + case AccessModeReadWriteMany: + // NFS volumes allow any number of rw/ro attachments — no restrictions. + + case AccessModeReadOnlyMany: if !req.Readonly { - return fmt.Errorf("cannot attach read-write: volume has existing read-only attachments") + return fmt.Errorf("cannot attach read-write: volume access mode is ReadOnlyMany") + } + + default: // ReadWriteOnce + if len(meta.Attachments) > 0 { + for _, att := range meta.Attachments { + if !att.Readonly { + return fmt.Errorf("volume has exclusive read-write attachment to instance %s", att.InstanceID) + } + } + if !req.Readonly { + return fmt.Errorf("cannot attach read-write: volume has existing read-only attachments") + } } } @@ -453,12 +519,26 @@ func (m *manager) DetachVolume(ctx context.Context, volumeID string, instanceID return saveMetadata(m.paths, meta) } -// GetVolumePath returns the path to the volume data file +// GetVolumePath returns the path to the volume data file. +// Returns empty string for NFS-backed volumes (no local data file). func (m *manager) GetVolumePath(id string) string { return m.paths.VolumeData(id) } +// IsNFSVolume returns true if the volume uses NFS backing. +func (m *manager) IsNFSVolume(ctx context.Context, id string) bool { + lock := m.getVolumeLock(id) + lock.RLock() + defer lock.RUnlock() + meta, err := loadMetadata(m.paths, id) + if err != nil { + return false + } + return meta.AccessMode == AccessModeReadWriteMany && meta.NFS != nil +} + // TotalVolumeBytes returns the total size of all volumes. +// NFS volumes are excluded (they don't consume local disk). func (m *manager) TotalVolumeBytes(ctx context.Context) (int64, error) { return m.getTotalVolumeBytes(ctx) } @@ -477,12 +557,29 @@ func (m *manager) metadataToVolume(meta *storedMetadata) *Volume { } } - return &Volume{ + accessMode := meta.AccessMode + if accessMode == "" { + accessMode = AccessModeReadWriteOnce + } + + vol := &Volume{ Id: meta.Id, Name: meta.Name, SizeGb: meta.SizeGb, + AccessMode: accessMode, Tags: tags.Clone(meta.Tags), CreatedAt: createdAt, Attachments: attachments, } + + if meta.NFS != nil { + vol.NFS = &NFSConfig{ + Server: meta.NFS.Server, + ExportPath: meta.NFS.ExportPath, + Version: meta.NFS.Version, + Options: meta.NFS.Options, + } + } + + return vol } diff --git a/lib/volumes/storage.go b/lib/volumes/storage.go index cd600ddb..55db5bf8 100644 --- a/lib/volumes/storage.go +++ b/lib/volumes/storage.go @@ -23,11 +23,21 @@ type storedAttachment struct { Readonly bool `json:"readonly"` } +// storedNFSConfig represents NFS configuration in stored metadata +type storedNFSConfig struct { + Server string `json:"server"` + ExportPath string `json:"export_path"` + Version string `json:"version,omitempty"` + Options string `json:"options,omitempty"` +} + // storedMetadata represents volume metadata that is persisted to disk type storedMetadata struct { Id string `json:"id"` Name string `json:"name"` SizeGb int `json:"size_gb"` + AccessMode AccessMode `json:"access_mode,omitempty"` // Empty treated as ReadWriteOnce + NFS *storedNFSConfig `json:"nfs,omitempty"` Tags tags.Tags `json:"tags,omitempty"` CreatedAt string `json:"created_at"` // RFC3339 format Attachments []storedAttachment `json:"attachments,omitempty"` diff --git a/lib/volumes/types.go b/lib/volumes/types.go index c6648bfb..82776b76 100644 --- a/lib/volumes/types.go +++ b/lib/volumes/types.go @@ -6,6 +6,30 @@ import ( "github.com/kernel/hypeman/lib/tags" ) +// AccessMode describes how a volume can be accessed by instances. +type AccessMode string + +const ( + // AccessModeReadWriteOnce allows a single instance to mount the volume read-write. + // This is the default for block-backed volumes. + AccessModeReadWriteOnce AccessMode = "ReadWriteOnce" + + // AccessModeReadOnlyMany allows multiple instances to mount the volume read-only. + AccessModeReadOnlyMany AccessMode = "ReadOnlyMany" + + // AccessModeReadWriteMany allows multiple instances to mount the volume read-write + // simultaneously. Requires an NFS backing store. + AccessModeReadWriteMany AccessMode = "ReadWriteMany" +) + +// NFSConfig contains NFS server connection details for ReadWriteMany volumes. +type NFSConfig struct { + Server string // NFS server hostname or IP address + ExportPath string // NFS export path (e.g., "/exports/shared-data") + Version string // NFS version (e.g., "4.1"); defaults to "4.1" if empty + Options string // Additional NFS mount options (e.g., "hard,timeo=600") +} + // Attachment represents a volume attached to an instance type Attachment struct { InstanceID string @@ -18,6 +42,8 @@ type Volume struct { Id string Name string SizeGb int + AccessMode AccessMode // Volume access mode (default: ReadWriteOnce) + NFS *NFSConfig // NFS configuration (only set when AccessMode is ReadWriteMany) Tags tags.Tags CreatedAt time.Time Attachments []Attachment // List of current attachments (empty if not attached) @@ -25,10 +51,12 @@ type Volume struct { // CreateVolumeRequest is the domain request for creating a volume type CreateVolumeRequest struct { - Name string - SizeGb int - Id *string // Optional custom ID - Tags tags.Tags + Name string + SizeGb int + AccessMode AccessMode // Optional; defaults to ReadWriteOnce + NFS *NFSConfig // Required when AccessMode is ReadWriteMany + Id *string // Optional custom ID + Tags tags.Tags } // AttachVolumeRequest is the domain request for attaching a volume to an instance @@ -46,3 +74,13 @@ type CreateVolumeFromArchiveRequest struct { Id *string // Optional custom ID Tags tags.Tags } + +// ValidAccessMode returns true if mode is a recognized AccessMode value. +func ValidAccessMode(mode AccessMode) bool { + switch mode { + case AccessModeReadWriteOnce, AccessModeReadOnlyMany, AccessModeReadWriteMany: + return true + default: + return false + } +} diff --git a/openapi.yaml b/openapi.yaml index b1884bac..20ab961a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1057,6 +1057,28 @@ components: description: Creation timestamp (RFC3339) example: "2025-01-15T10:00:00Z" + NFSConfig: + type: object + required: [server, export_path] + description: NFS server connection details for ReadWriteMany volumes. + properties: + server: + type: string + description: NFS server hostname or IP address + example: "nfs.internal.example.com" + export_path: + type: string + description: NFS export path + example: "/exports/shared-data" + version: + type: string + description: NFS protocol version (defaults to "4.1") + example: "4.1" + options: + type: string + description: Additional NFS mount options + example: "hard,timeo=600" + CreateVolumeRequest: type: object required: [name, size_gb] @@ -1073,6 +1095,17 @@ components: type: integer description: Size in gigabytes example: 10 + access_mode: + type: string + description: | + Volume access mode. Determines multi-attach behavior: + - ReadWriteOnce (default): single read-write or multiple read-only attachments + - ReadOnlyMany: multiple read-only attachments only + - ReadWriteMany: multiple read-write attachments (requires NFS config) + enum: [ReadWriteOnce, ReadOnlyMany, ReadWriteMany] + default: ReadWriteOnce + nfs: + $ref: "#/components/schemas/NFSConfig" tags: $ref: "#/components/schemas/Tags" @@ -1109,6 +1142,12 @@ components: type: integer description: Size in gigabytes example: 10 + access_mode: + type: string + description: Volume access mode + enum: [ReadWriteOnce, ReadOnlyMany, ReadWriteMany] + nfs: + $ref: "#/components/schemas/NFSConfig" tags: $ref: "#/components/schemas/Tags" attachments: