From 992855004aae489a4ac0c40802a928e6c8b33699 Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:59:05 +0000 Subject: [PATCH] Add ReadWriteMany (RWX) volume support via NFS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a volume access mode system with three modes: - ReadWriteOnce (default): single rw or multiple ro, block-backed - ReadOnlyMany: multiple ro only, block-backed - ReadWriteMany: multiple rw/ro via NFS backing NFS volumes don't create local disk images — they store server connection details (server, export path, version, options) in metadata and pass them through to the guest init config. The guest mounts volumes via NFS instead of block devices. Key changes: - Volume domain types: AccessMode enum, NFSConfig struct - Volume manager: RWX-aware attachment rules, NFS creation flow - Instance layer: NFS config propagation, device count skip for NFS - Guest config: "nfs"/"nfs_ro" mount modes with server details - OpenAPI spec: access_mode, nfs fields on create/response - Validation: NFS requires networking, no overlay for NFS volumes Co-Authored-By: Claude Opus 4.6 --- cmd/api/api/volumes.go | 56 +++- lib/instances/admission.go | 2 +- lib/instances/configdisk.go | 21 +- lib/instances/create.go | 54 +++- lib/instances/types.go | 19 +- lib/oapi/oapi.go | 589 ++++++++++++++++++++---------------- lib/vmconfig/config.go | 8 +- lib/volumes/errors.go | 9 +- lib/volumes/manager.go | 173 ++++++++--- lib/volumes/storage.go | 10 + lib/volumes/types.go | 46 ++- openapi.yaml | 39 +++ 12 files changed, 685 insertions(+), 341 deletions(-) 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: