diff --git a/api/resource/definitions/enums/enums.proto b/api/resource/definitions/enums/enums.proto index 4984c8905d0..0144c2509b1 100755 --- a/api/resource/definitions/enums/enums.proto +++ b/api/resource/definitions/enums/enums.proto @@ -705,6 +705,17 @@ enum BlockFilesystemType { FILESYSTEM_TYPE_EXT4 = 3; FILESYSTEM_TYPE_ISO9660 = 4; FILESYSTEM_TYPE_SWAP = 5; + FILESYSTEM_TYPE_NFS = 6; + FILESYSTEM_TYPE_VIRTIOFS = 7; +} + +// BlockNFSVersionType describes NFS version type. +enum BlockNFSVersionType { + NFS_VERSION_TYPE4_2 = 0; + NFS_VERSION_TYPE4_1 = 1; + NFS_VERSION_TYPE4 = 2; + NFS_VERSION_TYPE3 = 3; + NFS_VERSION_TYPE2 = 4; } // BlockVolumePhase describes volume phase. @@ -727,6 +738,7 @@ enum BlockVolumeType { VOLUME_TYPE_DIRECTORY = 3; VOLUME_TYPE_SYMLINK = 4; VOLUME_TYPE_OVERLAY = 5; + VOLUME_TYPE_EXTERNAL = 6; } // CriImageCacheStatus describes image cache status type. diff --git a/cmd/talosctl/cmd/mgmt/cluster/create/flags/virtiofs.go b/cmd/talosctl/cmd/mgmt/cluster/create/flags/virtiofs.go new file mode 100644 index 00000000000..d60309496cf --- /dev/null +++ b/cmd/talosctl/cmd/mgmt/cluster/create/flags/virtiofs.go @@ -0,0 +1,89 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package flags + +import ( + "errors" + "fmt" + "strings" +) + +// VirtiofsRequest is the configuration required for virtiofs share creation. +type VirtiofsRequest struct { + SharedDir string + SocketPath string +} + +// ParseVirtiofsFlag parses the virtiofs flag into a slice of VirtiofsRequest. +func ParseVirtiofsFlag(disks []string) ([]VirtiofsRequest, error) { + result := []VirtiofsRequest{} + + if len(disks) == 0 { + return nil, errors.New("at least one disk has to be specified") + } + + for _, d := range disks { + parts := strings.SplitN(d, ":", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid disk format: %q", d) + } + + result = append(result, VirtiofsRequest{ + SharedDir: parts[0], + SocketPath: parts[1], + }) + } + + return result, nil +} + +// Virtiofs implements pflag.Value for accumulating multiple VirtiofsRequest entries. +type Virtiofs struct { + requests []VirtiofsRequest +} + +// String returns a string representation suitable for flag printing. +func (f *Virtiofs) String() string { + if f == nil || len(f.requests) == 0 { + return "" + } + + parts := make([]string, 0, len(f.requests)) + for _, r := range f.requests { + parts = append(parts, fmt.Sprintf("%s:%s", r.SharedDir, r.SocketPath)) + } + + return strings.Join(parts, ",") +} + +// Set parses and appends one or more disk specifications to the flag value. +// The input may contain a single spec ("sharedDir:socketPath") or a comma-separated list. +func (f *Virtiofs) Set(value string) error { + if strings.TrimSpace(value) == "" { + return errors.New("virtiofs value must not be empty") + } + // Support comma-separated values in a single Set call. + raw := strings.Split(value, ",") + + reqs, err := ParseVirtiofsFlag(raw) + if err != nil { + return err + } + + f.requests = append(f.requests, reqs...) + + return nil +} + +// Type returns the flag's value type name. +func (f *Virtiofs) Type() string { return "virtiofs" } + +// Requests returns a defensive copy of the accumulated virtiofs share requests. +func (f *Virtiofs) Requests() []VirtiofsRequest { + out := make([]VirtiofsRequest, len(f.requests)) + copy(out, f.requests) + + return out +} diff --git a/cmd/talosctl/cmd/mgmt/cluster/create/flags/virtiofs_test.go b/cmd/talosctl/cmd/mgmt/cluster/create/flags/virtiofs_test.go new file mode 100644 index 00000000000..afe38cef966 --- /dev/null +++ b/cmd/talosctl/cmd/mgmt/cluster/create/flags/virtiofs_test.go @@ -0,0 +1,57 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package flags_test + +import ( + "testing" + + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + + flags "github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/cluster/create/flags" +) + +func TestVirtiofsFlag_AccumulatesAndRequests(t *testing.T) { + t.Parallel() + + var d flags.Virtiofs + + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.Var(&d, "virtiofs", "") + + args := []string{ + "--virtiofs", "/mnt/shared/1:/tmp/mnt-shared-1.sock", + "--virtiofs", "/mnt/shared/2:/tmp/mnt-shared-2.sock,/mnt/shared/3:/tmp/mnt-shared-3.sock", + } + + err := fs.Parse(args) + assert.NoError(t, err) + + reqs := d.Requests() + assert.Len(t, reqs, 3) + + assert.Equal(t, "/mnt/shared/1", reqs[0].SharedDir) + assert.Equal(t, "/tmp/mnt-shared-1.sock", reqs[0].SocketPath) + + assert.Equal(t, "/mnt/shared/2", reqs[1].SharedDir) + assert.Equal(t, "/tmp/mnt-shared-2.sock", reqs[1].SocketPath) + + assert.Equal(t, "/mnt/shared/3", reqs[2].SharedDir) + assert.Equal(t, "/tmp/mnt-shared-3.sock", reqs[2].SocketPath) + + // Type should be stable + assert.Equal(t, "virtiofs", d.Type()) + + assert.Equal(t, "/mnt/shared/1:/tmp/mnt-shared-1.sock,/mnt/shared/2:/tmp/mnt-shared-2.sock,/mnt/shared/3:/tmp/mnt-shared-3.sock", d.String()) +} + +func TestVirtiofsFlag_SetInvalid(t *testing.T) { + t.Parallel() + + var f flags.Virtiofs + + err := f.Set("invalid-no-colon") + assert.Error(t, err) +} diff --git a/cmd/talosctl/cmd/mgmt/nfsd_launch.go b/cmd/talosctl/cmd/mgmt/nfsd_launch.go new file mode 100644 index 00000000000..e173d5f28eb --- /dev/null +++ b/cmd/talosctl/cmd/mgmt/nfsd_launch.go @@ -0,0 +1,52 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//go:build linux || darwin + +package mgmt + +import ( + "net" + "strings" + + "github.com/spf13/cobra" + "golang.org/x/sync/errgroup" + + "github.com/siderolabs/talos/pkg/provision/providers/vm" +) + +var nfsdLaunchCmdFlags struct { + addr string + workdir string +} + +// nfsdLaunchCmd represents the nfsd-launch command. +var nfsdLaunchCmd = &cobra.Command{ + Use: "nfsd-launch", + Short: "Internal command used by VM provisioners", + Long: ``, + Args: cobra.NoArgs, + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + var ips []net.IP + + for ip := range strings.SplitSeq(dnsdLaunchCmdFlags.addr, ",") { + ips = append(ips, net.ParseIP(ip)) + } + + var eg errgroup.Group + + eg.Go(func() error { + return vm.NFSd(ips, dnsdLaunchCmdFlags.resolvConf) + }) + + return eg.Wait() + }, +} + +func init() { + nfsdLaunchCmd.Flags().StringVar(&nfsdLaunchCmdFlags.addr, "addr", "localhost:2049", `Address to bind nfsd to`) + nfsdLaunchCmd.Flags().StringVar(&nfsdLaunchCmdFlags.workdir, "workdir", "", `Working directory for nfsd`) + addCommand(nfsdLaunchCmd) +} diff --git a/cmd/talosctl/cmd/mgmt/virtiofsd_launch.go b/cmd/talosctl/cmd/mgmt/virtiofsd_launch.go new file mode 100644 index 00000000000..8830e763ad0 --- /dev/null +++ b/cmd/talosctl/cmd/mgmt/virtiofsd_launch.go @@ -0,0 +1,48 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//go:build linux || darwin + +package mgmt + +import ( + "github.com/spf13/cobra" + "golang.org/x/sync/errgroup" + + "github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/cluster/create/flags" + "github.com/siderolabs/talos/pkg/provision/providers/vm" +) + +var virtiofsdLaunchCmdFlags struct { + virtiofsdBin string + virtiofs flags.Virtiofs +} + +// virtiofsdLaunchCmd represents the virtiofsd-launch command. +var virtiofsdLaunchCmd = &cobra.Command{ + Use: "virtiofsd-launch", + Short: "Internal command used by VM provisioners", + Long: ``, + Args: cobra.NoArgs, + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + eg, ctx := errgroup.WithContext(cmd.Context()) + + for _, vfs := range virtiofsdLaunchCmdFlags.virtiofs.Requests() { + eg.Go(func() error { + return vm.Virtiofsd(ctx, virtiofsdLaunchCmdFlags.virtiofsdBin, vfs.SharedDir, vfs.SocketPath) + }) + } + + return eg.Wait() + }, +} + +func init() { + virtiofsdLaunchCmd.Flags().StringVar(&virtiofsdLaunchCmdFlags.virtiofsdBin, "bin", + "/usr/libexec/virtiofsd", `path to the virtiofsd binary`) + virtiofsdLaunchCmd.Flags().Var(&virtiofsdLaunchCmdFlags.virtiofs, "virtiofs", + `list of virtiofs shares to create in format ":"`) + addCommand(virtiofsdLaunchCmd) +} diff --git a/go.mod b/go.mod index c02a2049332..c3ff448b325 100644 --- a/go.mod +++ b/go.mod @@ -162,6 +162,7 @@ require ( github.com/siderolabs/siderolink v0.3.15 github.com/siderolabs/talos/pkg/machinery v1.12.0-alpha.2 github.com/sirupsen/logrus v1.9.3 + github.com/smallfz/libnfs-go v0.0.7 github.com/spf13/cobra v1.10.1 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index 9bce1960fa8..31a418586c0 100644 --- a/go.sum +++ b/go.sum @@ -671,6 +671,8 @@ github.com/siderolabs/wgctrl-go v0.0.0-20251029173431-c4fd5f6a4e72 h1:Boabco/vho github.com/siderolabs/wgctrl-go v0.0.0-20251029173431-c4fd5f6a4e72/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smallfz/libnfs-go v0.0.7 h1:7uS1ADMrJqihbQEtXfo8865+gEMl7bHKsDAY7ialKws= +github.com/smallfz/libnfs-go v0.0.7/go.mod h1:OtfrJ0akgDga0KIhtub2YJoDMnzHBntTAVwXdfNZZTU= github.com/smira/containerd/v2 v2.0.0-20251113120816-51ecd22de074 h1:05WpndnxEUV9TYnXWlR93QaeWHRk5+9cHeMmjGAK2CY= github.com/smira/containerd/v2 v2.0.0-20251113120816-51ecd22de074/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM= github.com/smira/kobject v0.0.0-20240304111826-49c8d4613389 h1:f/5NRv5IGZxbjBhc5MnlbNmyuXBPxvekhBAUzyKWyLY= diff --git a/internal/app/machined/pkg/controllers/block/internal/volumes/close.go b/internal/app/machined/pkg/controllers/block/internal/volumes/close.go index d27644f7b05..5ee17876cb3 100644 --- a/internal/app/machined/pkg/controllers/block/internal/volumes/close.go +++ b/internal/app/machined/pkg/controllers/block/internal/volumes/close.go @@ -18,7 +18,7 @@ import ( // Close the encrypted volumes. func Close(ctx context.Context, logger *zap.Logger, volumeContext ManagerContext) error { switch volumeContext.Cfg.TypedSpec().Type { - case block.VolumeTypeTmpfs, block.VolumeTypeDirectory, block.VolumeTypeSymlink, block.VolumeTypeOverlay: + case block.VolumeTypeTmpfs, block.VolumeTypeDirectory, block.VolumeTypeSymlink, block.VolumeTypeOverlay, block.VolumeTypeExternal: // volume types can be always closed volumeContext.Status.Phase = block.VolumePhaseClosed diff --git a/internal/app/machined/pkg/controllers/block/internal/volumes/locate.go b/internal/app/machined/pkg/controllers/block/internal/volumes/locate.go index 45cf6a69437..8986cd5fffe 100644 --- a/internal/app/machined/pkg/controllers/block/internal/volumes/locate.go +++ b/internal/app/machined/pkg/controllers/block/internal/volumes/locate.go @@ -31,7 +31,7 @@ func LocateAndProvision(ctx context.Context, logger *zap.Logger, volumeContext M volumeType := volumeContext.Cfg.TypedSpec().Type switch volumeType { - case block.VolumeTypeTmpfs, block.VolumeTypeDirectory, block.VolumeTypeSymlink, block.VolumeTypeOverlay: + case block.VolumeTypeTmpfs, block.VolumeTypeDirectory, block.VolumeTypeSymlink, block.VolumeTypeOverlay, block.VolumeTypeExternal: // volume types above are always ready volumeContext.Status.Phase = block.VolumePhaseReady diff --git a/internal/app/machined/pkg/controllers/block/internal/volumes/volumeconfig/user_volumes.go b/internal/app/machined/pkg/controllers/block/internal/volumes/volumeconfig/user_volumes.go index 86925a518cc..2a264884e8b 100644 --- a/internal/app/machined/pkg/controllers/block/internal/volumes/volumeconfig/user_volumes.go +++ b/internal/app/machined/pkg/controllers/block/internal/volumes/volumeconfig/user_volumes.go @@ -28,6 +28,7 @@ var UserVolumeTransformers = []volumeConfigTransformer{ UserVolumeTransformer, RawVolumeTransformer, ExistingVolumeTransformer, + ExternalVolumeTransformer, SwapVolumeTransformer, } @@ -123,7 +124,7 @@ func UserVolumeTransformer(c configconfig.Config) ([]VolumeResource, error) { WithConvertEncryptionConfiguration(userVolumeConfig.Encryption()). WriterFunc() - case block.VolumeTypeTmpfs, block.VolumeTypeSymlink, block.VolumeTypeOverlay: + case block.VolumeTypeTmpfs, block.VolumeTypeSymlink, block.VolumeTypeOverlay, block.VolumeTypeExternal: fallthrough default: @@ -210,6 +211,40 @@ func ExistingVolumeTransformer(c configconfig.Config) ([]VolumeResource, error) return resources, nil } +// ExternalVolumeTransformer is the transformer for external user volume configs. +func ExternalVolumeTransformer(c configconfig.Config) ([]VolumeResource, error) { + if c == nil { + return nil, nil + } + + resources := make([]VolumeResource, 0, len(c.ExternalVolumeConfigs())) + + for _, externalVolumeConfig := range c.ExternalVolumeConfigs() { + volumeID := constants.ExternalVolumePrefix + externalVolumeConfig.Name() + resources = append(resources, VolumeResource{ + VolumeID: volumeID, + Label: block.ExternalVolumeLabel, + TransformFunc: NewBuilder(). + WithType(block.VolumeTypeExternal). + WithProvisioning(block.ProvisioningSpec{ + Wave: block.WaveUserVolumes, + }). + WithMount(block.MountSpec{ + TargetPath: externalVolumeConfig.Name(), + ParentID: constants.UserVolumeMountPoint, + SelinuxLabel: constants.EphemeralSelinuxLabel, + FileMode: 0o755, + UID: 0, + GID: 0, + }). + WriterFunc(), + MountTransformFunc: HandleExternalVolumeMountRequest(externalVolumeConfig), + }) + } + + return resources, nil +} + // SwapVolumeTransformer is the transformer for swap volume configs. func SwapVolumeTransformer(c configconfig.Config) ([]VolumeResource, error) { if c == nil { @@ -261,6 +296,15 @@ func HandleExistingVolumeMountRequest(existingVolumeConfig configconfig.Existing } } +// HandleExternalVolumeMountRequest returns a MountTransformFunc for external volumes. +func HandleExternalVolumeMountRequest(externalVolumeConfig configconfig.ExternalVolumeConfig) func(m *block.VolumeMountRequest) error { + return func(m *block.VolumeMountRequest) error { + m.TypedSpec().ReadOnly = externalVolumeConfig.Mount().ReadOnly() + + return nil + } +} + // DefaultMountTransform is a no-op. func DefaultMountTransform(_ *block.VolumeMountRequest) error { return nil diff --git a/internal/app/machined/pkg/controllers/block/mount.go b/internal/app/machined/pkg/controllers/block/mount.go index 3d43d08724a..fb7f0a47ba5 100644 --- a/internal/app/machined/pkg/controllers/block/mount.go +++ b/internal/app/machined/pkg/controllers/block/mount.go @@ -300,7 +300,7 @@ func (ctrl *MountController) handleMountOperation( return ctrl.handleSymlinkMountOperation(logger, rootPath, mountTarget, mountRequest, volumeStatus) case block.VolumeTypeTmpfs: return fmt.Errorf("not implemented yet") - case block.VolumeTypeDisk, block.VolumeTypePartition: + case block.VolumeTypeDisk, block.VolumeTypePartition, block.VolumeTypeExternal: // TODO(shanduur) if mountFilesystem == block.FilesystemTypeSwap { return ctrl.handleSwapMountOperation(logger, mountSource, mountRequest, volumeStatus) } @@ -734,7 +734,7 @@ func (ctrl *MountController) handleUnmountOperation( return ctrl.handleDirectoryUnmountOperation(logger, mountRequest, volumeStatus) case block.VolumeTypeTmpfs: return fmt.Errorf("not implemented yet") - case block.VolumeTypeDisk, block.VolumeTypePartition, block.VolumeTypeOverlay: + case block.VolumeTypeDisk, block.VolumeTypePartition, block.VolumeTypeOverlay, block.VolumeTypeExternal: // TODO(shanduur) if volumeStatus.TypedSpec().Filesystem == block.FilesystemTypeSwap { return ctrl.handleSwapUmountOperation(logger, mountRequest, volumeStatus) } diff --git a/internal/app/machined/pkg/controllers/hardware/pcr_status.go b/internal/app/machined/pkg/controllers/hardware/pcr_status.go index aac484117e0..88c0b48510e 100644 --- a/internal/app/machined/pkg/controllers/hardware/pcr_status.go +++ b/internal/app/machined/pkg/controllers/hardware/pcr_status.go @@ -100,7 +100,7 @@ func (ctrl *PCRStatusController) Run(ctx context.Context, r controller.Runtime, switch volumeStatus.TypedSpec().Type { case block.VolumeTypeDisk, block.VolumeTypePartition: // can be encrypted - case block.VolumeTypeDirectory, block.VolumeTypeOverlay, block.VolumeTypeSymlink, block.VolumeTypeTmpfs: + case block.VolumeTypeDirectory, block.VolumeTypeOverlay, block.VolumeTypeSymlink, block.VolumeTypeTmpfs, block.VolumeTypeExternal: // skip it, not encryptable continue } diff --git a/pkg/machinery/api/resource/definitions/enums/enums.pb.go b/pkg/machinery/api/resource/definitions/enums/enums.pb.go index 8e5dd2f76da..1d05f3eab5a 100644 --- a/pkg/machinery/api/resource/definitions/enums/enums.pb.go +++ b/pkg/machinery/api/resource/definitions/enums/enums.pb.go @@ -3184,12 +3184,14 @@ func (BlockEncryptionProviderType) EnumDescriptor() ([]byte, []int) { type BlockFilesystemType int32 const ( - BlockFilesystemType_FILESYSTEM_TYPE_NONE BlockFilesystemType = 0 - BlockFilesystemType_FILESYSTEM_TYPE_XFS BlockFilesystemType = 1 - BlockFilesystemType_FILESYSTEM_TYPE_VFAT BlockFilesystemType = 2 - BlockFilesystemType_FILESYSTEM_TYPE_EXT4 BlockFilesystemType = 3 - BlockFilesystemType_FILESYSTEM_TYPE_ISO9660 BlockFilesystemType = 4 - BlockFilesystemType_FILESYSTEM_TYPE_SWAP BlockFilesystemType = 5 + BlockFilesystemType_FILESYSTEM_TYPE_NONE BlockFilesystemType = 0 + BlockFilesystemType_FILESYSTEM_TYPE_XFS BlockFilesystemType = 1 + BlockFilesystemType_FILESYSTEM_TYPE_VFAT BlockFilesystemType = 2 + BlockFilesystemType_FILESYSTEM_TYPE_EXT4 BlockFilesystemType = 3 + BlockFilesystemType_FILESYSTEM_TYPE_ISO9660 BlockFilesystemType = 4 + BlockFilesystemType_FILESYSTEM_TYPE_SWAP BlockFilesystemType = 5 + BlockFilesystemType_FILESYSTEM_TYPE_NFS BlockFilesystemType = 6 + BlockFilesystemType_FILESYSTEM_TYPE_VIRTIOFS BlockFilesystemType = 7 ) // Enum value maps for BlockFilesystemType. @@ -3201,14 +3203,18 @@ var ( 3: "FILESYSTEM_TYPE_EXT4", 4: "FILESYSTEM_TYPE_ISO9660", 5: "FILESYSTEM_TYPE_SWAP", + 6: "FILESYSTEM_TYPE_NFS", + 7: "FILESYSTEM_TYPE_VIRTIOFS", } BlockFilesystemType_value = map[string]int32{ - "FILESYSTEM_TYPE_NONE": 0, - "FILESYSTEM_TYPE_XFS": 1, - "FILESYSTEM_TYPE_VFAT": 2, - "FILESYSTEM_TYPE_EXT4": 3, - "FILESYSTEM_TYPE_ISO9660": 4, - "FILESYSTEM_TYPE_SWAP": 5, + "FILESYSTEM_TYPE_NONE": 0, + "FILESYSTEM_TYPE_XFS": 1, + "FILESYSTEM_TYPE_VFAT": 2, + "FILESYSTEM_TYPE_EXT4": 3, + "FILESYSTEM_TYPE_ISO9660": 4, + "FILESYSTEM_TYPE_SWAP": 5, + "FILESYSTEM_TYPE_NFS": 6, + "FILESYSTEM_TYPE_VIRTIOFS": 7, } ) @@ -3239,6 +3245,62 @@ func (BlockFilesystemType) EnumDescriptor() ([]byte, []int) { return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{38} } +// BlockNFSVersionType describes NFS version type. +type BlockNFSVersionType int32 + +const ( + BlockNFSVersionType_NFS_VERSION_TYPE4_2 BlockNFSVersionType = 0 + BlockNFSVersionType_NFS_VERSION_TYPE4_1 BlockNFSVersionType = 1 + BlockNFSVersionType_NFS_VERSION_TYPE4 BlockNFSVersionType = 2 + BlockNFSVersionType_NFS_VERSION_TYPE3 BlockNFSVersionType = 3 + BlockNFSVersionType_NFS_VERSION_TYPE2 BlockNFSVersionType = 4 +) + +// Enum value maps for BlockNFSVersionType. +var ( + BlockNFSVersionType_name = map[int32]string{ + 0: "NFS_VERSION_TYPE4_2", + 1: "NFS_VERSION_TYPE4_1", + 2: "NFS_VERSION_TYPE4", + 3: "NFS_VERSION_TYPE3", + 4: "NFS_VERSION_TYPE2", + } + BlockNFSVersionType_value = map[string]int32{ + "NFS_VERSION_TYPE4_2": 0, + "NFS_VERSION_TYPE4_1": 1, + "NFS_VERSION_TYPE4": 2, + "NFS_VERSION_TYPE3": 3, + "NFS_VERSION_TYPE2": 4, + } +) + +func (x BlockNFSVersionType) Enum() *BlockNFSVersionType { + p := new(BlockNFSVersionType) + *p = x + return p +} + +func (x BlockNFSVersionType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (BlockNFSVersionType) Descriptor() protoreflect.EnumDescriptor { + return file_resource_definitions_enums_enums_proto_enumTypes[39].Descriptor() +} + +func (BlockNFSVersionType) Type() protoreflect.EnumType { + return &file_resource_definitions_enums_enums_proto_enumTypes[39] +} + +func (x BlockNFSVersionType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use BlockNFSVersionType.Descriptor instead. +func (BlockNFSVersionType) EnumDescriptor() ([]byte, []int) { + return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{39} +} + // BlockVolumePhase describes volume phase. type BlockVolumePhase int32 @@ -3288,11 +3350,11 @@ func (x BlockVolumePhase) String() string { } func (BlockVolumePhase) Descriptor() protoreflect.EnumDescriptor { - return file_resource_definitions_enums_enums_proto_enumTypes[39].Descriptor() + return file_resource_definitions_enums_enums_proto_enumTypes[40].Descriptor() } func (BlockVolumePhase) Type() protoreflect.EnumType { - return &file_resource_definitions_enums_enums_proto_enumTypes[39] + return &file_resource_definitions_enums_enums_proto_enumTypes[40] } func (x BlockVolumePhase) Number() protoreflect.EnumNumber { @@ -3301,7 +3363,7 @@ func (x BlockVolumePhase) Number() protoreflect.EnumNumber { // Deprecated: Use BlockVolumePhase.Descriptor instead. func (BlockVolumePhase) EnumDescriptor() ([]byte, []int) { - return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{39} + return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{40} } // BlockVolumeType describes volume type. @@ -3314,6 +3376,7 @@ const ( BlockVolumeType_VOLUME_TYPE_DIRECTORY BlockVolumeType = 3 BlockVolumeType_VOLUME_TYPE_SYMLINK BlockVolumeType = 4 BlockVolumeType_VOLUME_TYPE_OVERLAY BlockVolumeType = 5 + BlockVolumeType_VOLUME_TYPE_EXTERNAL BlockVolumeType = 6 ) // Enum value maps for BlockVolumeType. @@ -3325,6 +3388,7 @@ var ( 3: "VOLUME_TYPE_DIRECTORY", 4: "VOLUME_TYPE_SYMLINK", 5: "VOLUME_TYPE_OVERLAY", + 6: "VOLUME_TYPE_EXTERNAL", } BlockVolumeType_value = map[string]int32{ "VOLUME_TYPE_PARTITION": 0, @@ -3333,6 +3397,7 @@ var ( "VOLUME_TYPE_DIRECTORY": 3, "VOLUME_TYPE_SYMLINK": 4, "VOLUME_TYPE_OVERLAY": 5, + "VOLUME_TYPE_EXTERNAL": 6, } ) @@ -3347,11 +3412,11 @@ func (x BlockVolumeType) String() string { } func (BlockVolumeType) Descriptor() protoreflect.EnumDescriptor { - return file_resource_definitions_enums_enums_proto_enumTypes[40].Descriptor() + return file_resource_definitions_enums_enums_proto_enumTypes[41].Descriptor() } func (BlockVolumeType) Type() protoreflect.EnumType { - return &file_resource_definitions_enums_enums_proto_enumTypes[40] + return &file_resource_definitions_enums_enums_proto_enumTypes[41] } func (x BlockVolumeType) Number() protoreflect.EnumNumber { @@ -3360,7 +3425,7 @@ func (x BlockVolumeType) Number() protoreflect.EnumNumber { // Deprecated: Use BlockVolumeType.Descriptor instead. func (BlockVolumeType) EnumDescriptor() ([]byte, []int) { - return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{40} + return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{41} } // CriImageCacheStatus describes image cache status type. @@ -3400,11 +3465,11 @@ func (x CriImageCacheStatus) String() string { } func (CriImageCacheStatus) Descriptor() protoreflect.EnumDescriptor { - return file_resource_definitions_enums_enums_proto_enumTypes[41].Descriptor() + return file_resource_definitions_enums_enums_proto_enumTypes[42].Descriptor() } func (CriImageCacheStatus) Type() protoreflect.EnumType { - return &file_resource_definitions_enums_enums_proto_enumTypes[41] + return &file_resource_definitions_enums_enums_proto_enumTypes[42] } func (x CriImageCacheStatus) Number() protoreflect.EnumNumber { @@ -3413,7 +3478,7 @@ func (x CriImageCacheStatus) Number() protoreflect.EnumNumber { // Deprecated: Use CriImageCacheStatus.Descriptor instead. func (CriImageCacheStatus) EnumDescriptor() ([]byte, []int) { - return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{41} + return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{42} } // CriImageCacheCopyStatus describes image cache copy status type. @@ -3453,11 +3518,11 @@ func (x CriImageCacheCopyStatus) String() string { } func (CriImageCacheCopyStatus) Descriptor() protoreflect.EnumDescriptor { - return file_resource_definitions_enums_enums_proto_enumTypes[42].Descriptor() + return file_resource_definitions_enums_enums_proto_enumTypes[43].Descriptor() } func (CriImageCacheCopyStatus) Type() protoreflect.EnumType { - return &file_resource_definitions_enums_enums_proto_enumTypes[42] + return &file_resource_definitions_enums_enums_proto_enumTypes[43] } func (x CriImageCacheCopyStatus) Number() protoreflect.EnumNumber { @@ -3466,7 +3531,7 @@ func (x CriImageCacheCopyStatus) Number() protoreflect.EnumNumber { // Deprecated: Use CriImageCacheCopyStatus.Descriptor instead. func (CriImageCacheCopyStatus) EnumDescriptor() ([]byte, []int) { - return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{42} + return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{43} } // KubespanPeerState is KubeSpan peer current state. @@ -3503,11 +3568,11 @@ func (x KubespanPeerState) String() string { } func (KubespanPeerState) Descriptor() protoreflect.EnumDescriptor { - return file_resource_definitions_enums_enums_proto_enumTypes[43].Descriptor() + return file_resource_definitions_enums_enums_proto_enumTypes[44].Descriptor() } func (KubespanPeerState) Type() protoreflect.EnumType { - return &file_resource_definitions_enums_enums_proto_enumTypes[43] + return &file_resource_definitions_enums_enums_proto_enumTypes[44] } func (x KubespanPeerState) Number() protoreflect.EnumNumber { @@ -3516,7 +3581,7 @@ func (x KubespanPeerState) Number() protoreflect.EnumNumber { // Deprecated: Use KubespanPeerState.Descriptor instead. func (KubespanPeerState) EnumDescriptor() ([]byte, []int) { - return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{43} + return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{44} } // NetworkConfigLayer describes network configuration layers, with lowest priority first. @@ -3559,11 +3624,11 @@ func (x NetworkConfigLayer) String() string { } func (NetworkConfigLayer) Descriptor() protoreflect.EnumDescriptor { - return file_resource_definitions_enums_enums_proto_enumTypes[44].Descriptor() + return file_resource_definitions_enums_enums_proto_enumTypes[45].Descriptor() } func (NetworkConfigLayer) Type() protoreflect.EnumType { - return &file_resource_definitions_enums_enums_proto_enumTypes[44] + return &file_resource_definitions_enums_enums_proto_enumTypes[45] } func (x NetworkConfigLayer) Number() protoreflect.EnumNumber { @@ -3572,7 +3637,7 @@ func (x NetworkConfigLayer) Number() protoreflect.EnumNumber { // Deprecated: Use NetworkConfigLayer.Descriptor instead. func (NetworkConfigLayer) EnumDescriptor() ([]byte, []int) { - return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{44} + return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{45} } // NetworkOperator enumerates Talos network operators. @@ -3609,11 +3674,11 @@ func (x NetworkOperator) String() string { } func (NetworkOperator) Descriptor() protoreflect.EnumDescriptor { - return file_resource_definitions_enums_enums_proto_enumTypes[45].Descriptor() + return file_resource_definitions_enums_enums_proto_enumTypes[46].Descriptor() } func (NetworkOperator) Type() protoreflect.EnumType { - return &file_resource_definitions_enums_enums_proto_enumTypes[45] + return &file_resource_definitions_enums_enums_proto_enumTypes[46] } func (x NetworkOperator) Number() protoreflect.EnumNumber { @@ -3622,7 +3687,7 @@ func (x NetworkOperator) Number() protoreflect.EnumNumber { // Deprecated: Use NetworkOperator.Descriptor instead. func (NetworkOperator) EnumDescriptor() ([]byte, []int) { - return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{45} + return file_resource_definitions_enums_enums_proto_rawDescGZIP(), []int{46} } var File_resource_definitions_enums_enums_proto protoreflect.FileDescriptor @@ -4233,14 +4298,22 @@ const file_resource_definitions_enums_enums_proto_rawDesc = "" + "\x12ENCRYPTION_KEY_TPM\x10\x03*Z\n" + "\x1bBlockEncryptionProviderType\x12\x1c\n" + "\x18ENCRYPTION_PROVIDER_NONE\x10\x00\x12\x1d\n" + - "\x19ENCRYPTION_PROVIDER_LUKS2\x10\x01*\xb3\x01\n" + + "\x19ENCRYPTION_PROVIDER_LUKS2\x10\x01*\xea\x01\n" + "\x13BlockFilesystemType\x12\x18\n" + "\x14FILESYSTEM_TYPE_NONE\x10\x00\x12\x17\n" + "\x13FILESYSTEM_TYPE_XFS\x10\x01\x12\x18\n" + "\x14FILESYSTEM_TYPE_VFAT\x10\x02\x12\x18\n" + "\x14FILESYSTEM_TYPE_EXT4\x10\x03\x12\x1b\n" + "\x17FILESYSTEM_TYPE_ISO9660\x10\x04\x12\x18\n" + - "\x14FILESYSTEM_TYPE_SWAP\x10\x05*\xe3\x01\n" + + "\x14FILESYSTEM_TYPE_SWAP\x10\x05\x12\x17\n" + + "\x13FILESYSTEM_TYPE_NFS\x10\x06\x12\x1c\n" + + "\x18FILESYSTEM_TYPE_VIRTIOFS\x10\a*\x8c\x01\n" + + "\x13BlockNFSVersionType\x12\x17\n" + + "\x13NFS_VERSION_TYPE4_2\x10\x00\x12\x17\n" + + "\x13NFS_VERSION_TYPE4_1\x10\x01\x12\x15\n" + + "\x11NFS_VERSION_TYPE4\x10\x02\x12\x15\n" + + "\x11NFS_VERSION_TYPE3\x10\x03\x12\x15\n" + + "\x11NFS_VERSION_TYPE2\x10\x04*\xe3\x01\n" + "\x10BlockVolumePhase\x12\x18\n" + "\x14VOLUME_PHASE_WAITING\x10\x00\x12\x17\n" + "\x13VOLUME_PHASE_FAILED\x10\x01\x12\x18\n" + @@ -4249,14 +4322,15 @@ const file_resource_definitions_enums_enums_proto_rawDesc = "" + "\x18VOLUME_PHASE_PROVISIONED\x10\x04\x12\x19\n" + "\x15VOLUME_PHASE_PREPARED\x10\x05\x12\x16\n" + "\x12VOLUME_PHASE_READY\x10\x06\x12\x17\n" + - "\x13VOLUME_PHASE_CLOSED\x10\a*\xa6\x01\n" + + "\x13VOLUME_PHASE_CLOSED\x10\a*\xc0\x01\n" + "\x0fBlockVolumeType\x12\x19\n" + "\x15VOLUME_TYPE_PARTITION\x10\x00\x12\x14\n" + "\x10VOLUME_TYPE_DISK\x10\x01\x12\x15\n" + "\x11VOLUME_TYPE_TMPFS\x10\x02\x12\x19\n" + "\x15VOLUME_TYPE_DIRECTORY\x10\x03\x12\x17\n" + "\x13VOLUME_TYPE_SYMLINK\x10\x04\x12\x17\n" + - "\x13VOLUME_TYPE_OVERLAY\x10\x05*\x96\x01\n" + + "\x13VOLUME_TYPE_OVERLAY\x10\x05\x12\x18\n" + + "\x14VOLUME_TYPE_EXTERNAL\x10\x06*\x96\x01\n" + "\x13CriImageCacheStatus\x12\x1e\n" + "\x1aIMAGE_CACHE_STATUS_UNKNOWN\x10\x00\x12\x1f\n" + "\x1bIMAGE_CACHE_STATUS_DISABLED\x10\x01\x12 \n" + @@ -4295,7 +4369,7 @@ func file_resource_definitions_enums_enums_proto_rawDescGZIP() []byte { return file_resource_definitions_enums_enums_proto_rawDescData } -var file_resource_definitions_enums_enums_proto_enumTypes = make([]protoimpl.EnumInfo, 46) +var file_resource_definitions_enums_enums_proto_enumTypes = make([]protoimpl.EnumInfo, 47) var file_resource_definitions_enums_enums_proto_goTypes = []any{ (RuntimeMachineStage)(0), // 0: talos.resource.definitions.enums.RuntimeMachineStage (RuntimeSELinuxState)(0), // 1: talos.resource.definitions.enums.RuntimeSELinuxState @@ -4336,13 +4410,14 @@ var file_resource_definitions_enums_enums_proto_goTypes = []any{ (BlockEncryptionKeyType)(0), // 36: talos.resource.definitions.enums.BlockEncryptionKeyType (BlockEncryptionProviderType)(0), // 37: talos.resource.definitions.enums.BlockEncryptionProviderType (BlockFilesystemType)(0), // 38: talos.resource.definitions.enums.BlockFilesystemType - (BlockVolumePhase)(0), // 39: talos.resource.definitions.enums.BlockVolumePhase - (BlockVolumeType)(0), // 40: talos.resource.definitions.enums.BlockVolumeType - (CriImageCacheStatus)(0), // 41: talos.resource.definitions.enums.CriImageCacheStatus - (CriImageCacheCopyStatus)(0), // 42: talos.resource.definitions.enums.CriImageCacheCopyStatus - (KubespanPeerState)(0), // 43: talos.resource.definitions.enums.KubespanPeerState - (NetworkConfigLayer)(0), // 44: talos.resource.definitions.enums.NetworkConfigLayer - (NetworkOperator)(0), // 45: talos.resource.definitions.enums.NetworkOperator + (BlockNFSVersionType)(0), // 39: talos.resource.definitions.enums.BlockNFSVersionType + (BlockVolumePhase)(0), // 40: talos.resource.definitions.enums.BlockVolumePhase + (BlockVolumeType)(0), // 41: talos.resource.definitions.enums.BlockVolumeType + (CriImageCacheStatus)(0), // 42: talos.resource.definitions.enums.CriImageCacheStatus + (CriImageCacheCopyStatus)(0), // 43: talos.resource.definitions.enums.CriImageCacheCopyStatus + (KubespanPeerState)(0), // 44: talos.resource.definitions.enums.KubespanPeerState + (NetworkConfigLayer)(0), // 45: talos.resource.definitions.enums.NetworkConfigLayer + (NetworkOperator)(0), // 46: talos.resource.definitions.enums.NetworkOperator } var file_resource_definitions_enums_enums_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type @@ -4362,7 +4437,7 @@ func file_resource_definitions_enums_enums_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_resource_definitions_enums_enums_proto_rawDesc), len(file_resource_definitions_enums_enums_proto_rawDesc)), - NumEnums: 46, + NumEnums: 47, NumMessages: 0, NumExtensions: 0, NumServices: 0, diff --git a/pkg/machinery/config/config/config.go b/pkg/machinery/config/config/config.go index 9ed3e186933..31bcb1a6bcc 100644 --- a/pkg/machinery/config/config/config.go +++ b/pkg/machinery/config/config/config.go @@ -35,6 +35,7 @@ type Config interface { //nolint:interfacebloat UserVolumeConfigs() []UserVolumeConfig RawVolumeConfigs() []RawVolumeConfig ExistingVolumeConfigs() []ExistingVolumeConfig + ExternalVolumeConfigs() []ExternalVolumeConfig SwapVolumeConfigs() []SwapVolumeConfig ZswapConfig() ZswapConfig diff --git a/pkg/machinery/config/config/volume.go b/pkg/machinery/config/config/volume.go index 3ae24c4e36c..3d6fd3b0d60 100644 --- a/pkg/machinery/config/config/volume.go +++ b/pkg/machinery/config/config/volume.go @@ -116,6 +116,14 @@ type ExistingVolumeConfig interface { Mount() VolumeMountConfig } +// ExternalVolumeConfig defines the interface to access external volume configuration. +type ExternalVolumeConfig interface { + NamedDocument + ExternalVolumeConfigSignal() + Type() block.FilesystemType + Mount() ExternalMountConfig +} + // VolumeDiscoveryConfig defines the interface to discover volumes. type VolumeDiscoveryConfig interface { VolumeSelector() cel.Expression @@ -126,6 +134,18 @@ type VolumeMountConfig interface { ReadOnly() bool } +// ExternalMountConfig defines the interface to access volume mount configuration. +type ExternalMountConfig interface { + ReadOnly() bool + Source() string + NFS() optional.Optional[NFSMountConfig] +} + +// NFSMountConfig defines the interface to access NFS mount configuration. +type NFSMountConfig interface { + Version() string +} + // FilesystemConfig defines the interface to access filesystem configuration. type FilesystemConfig interface { // Type returns the filesystem type. diff --git a/pkg/machinery/config/container/container.go b/pkg/machinery/config/container/container.go index 8a472da79eb..d55943123c0 100644 --- a/pkg/machinery/config/container/container.go +++ b/pkg/machinery/config/container/container.go @@ -253,6 +253,11 @@ func (container *Container) UserVolumeConfigs() []config.UserVolumeConfig { return findMatchingDocs[config.UserVolumeConfig](container.documents) } +// ExternalVolumeConfigs implements config.Config interface. +func (container *Container) ExternalVolumeConfigs() []config.ExternalVolumeConfig { + return findMatchingDocs[config.ExternalVolumeConfig](container.documents) +} + // RawVolumeConfigs implements config.Config interface. func (container *Container) RawVolumeConfigs() []config.RawVolumeConfig { return findMatchingDocs[config.RawVolumeConfig](container.documents) diff --git a/pkg/machinery/config/schemas/config.schema.json b/pkg/machinery/config/schemas/config.schema.json index 25586cf48b3..2580cce1819 100644 --- a/pkg/machinery/config/schemas/config.schema.json +++ b/pkg/machinery/config/schemas/config.schema.json @@ -249,6 +249,87 @@ ], "description": "ExistingVolumeConfig is an existing volume configuration document.\\nExisting volumes allow to mount partitions (or whole disks) that were created\\noutside of Talos. Volume will be mounted under `/var/mnt/\u003cname\u003e`.\\nThe existing volume config name should not conflict with user volume names.\\n" }, + "block.ExternalMountSpec": { + "properties": { + "readOnly": { + "type": "boolean", + "title": "readOnly", + "description": "Mount the volume read-only.\n", + "markdownDescription": "Mount the volume read-only.", + "x-intellij-html-description": "\u003cp\u003eMount the volume read-only.\u003c/p\u003e\n" + }, + "source": { + "type": "string", + "title": "source", + "description": "Source of the volume.\n", + "markdownDescription": "Source of the volume.", + "x-intellij-html-description": "\u003cp\u003eSource of the volume.\u003c/p\u003e\n" + }, + "nfs": { + "$ref": "#/$defs/block.NFSMountSpec", + "title": "nfs", + "description": "NFS mount options.\n", + "markdownDescription": "NFS mount options.", + "x-intellij-html-description": "\u003cp\u003eNFS mount options.\u003c/p\u003e\n" + } + }, + "additionalProperties": false, + "type": "object", + "description": "ExternalMountSpec describes how the external volume is mounted." + }, + "block.ExternalVolumeConfigV1Alpha1": { + "properties": { + "apiVersion": { + "enum": [ + "v1alpha1" + ], + "title": "apiVersion", + "description": "apiVersion is the API version of the resource.\n", + "markdownDescription": "apiVersion is the API version of the resource.", + "x-intellij-html-description": "\u003cp\u003eapiVersion is the API version of the resource.\u003c/p\u003e\n" + }, + "kind": { + "enum": [ + "ExternalVolumeConfig" + ], + "title": "kind", + "description": "kind is the kind of the resource.\n", + "markdownDescription": "kind is the kind of the resource.", + "x-intellij-html-description": "\u003cp\u003ekind is the kind of the resource.\u003c/p\u003e\n" + }, + "name": { + "type": "string", + "title": "name", + "description": "Name of the mount.\n\nName might be between 1 and 34 characters long and can only contain:\nlowercase and uppercase ASCII letters, digits, and hyphens.\n", + "markdownDescription": "Name of the mount.\n\nName might be between 1 and 34 characters long and can only contain:\nlowercase and uppercase ASCII letters, digits, and hyphens.", + "x-intellij-html-description": "\u003cp\u003eName of the mount.\u003c/p\u003e\n\n\u003cp\u003eName might be between 1 and 34 characters long and can only contain:\nlowercase and uppercase ASCII letters, digits, and hyphens.\u003c/p\u003e\n" + }, + "filesystemType": { + "enum": [ + "virtiofs", + "nfs" + ], + "title": "filesystemType", + "description": "Filesystem type.\n", + "markdownDescription": "Filesystem type.", + "x-intellij-html-description": "\u003cp\u003eFilesystem type.\u003c/p\u003e\n" + }, + "mount": { + "$ref": "#/$defs/block.ExternalMountSpec", + "title": "mount", + "description": "The mount describes additional mount options.\n", + "markdownDescription": "The mount describes additional mount options.", + "x-intellij-html-description": "\u003cp\u003eThe mount describes additional mount options.\u003c/p\u003e\n" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "apiVersion", + "kind" + ], + "description": "ExternalVolumeConfig is a external disk mount configuration document.\\nExternal volumes allow to mount volumes that were created outside of Talos,\\nover the network or API. Volume will be mounted under `/var/mnt/\u003cname\u003e`.\\nThe external volume config name should not conflict with user volume names.\\n" + }, "block.FilesystemSpec": { "properties": { "type": { @@ -287,6 +368,26 @@ "type": "object", "description": "MountSpec describes how the volume is mounted." }, + "block.NFSMountSpec": { + "properties": { + "version": { + "enum": [ + "4.2", + "4.1", + "4", + "3", + "2" + ], + "title": "version", + "description": "NFS version to use.\n", + "markdownDescription": "NFS version to use.", + "x-intellij-html-description": "\u003cp\u003eNFS version to use.\u003c/p\u003e\n" + } + }, + "additionalProperties": false, + "type": "object", + "description": "NFSMountSpec describes NFS mount options." + }, "block.ProvisioningSpec": { "properties": { "diskSelector": { @@ -5128,6 +5229,9 @@ { "$ref": "#/$defs/block.ExistingVolumeConfigV1Alpha1" }, + { + "$ref": "#/$defs/block.ExternalVolumeConfigV1Alpha1" + }, { "$ref": "#/$defs/block.RawVolumeConfigV1Alpha1" }, diff --git a/pkg/machinery/config/types/block/block.go b/pkg/machinery/config/types/block/block.go index 55235aa0603..dd18d80a038 100644 --- a/pkg/machinery/config/types/block/block.go +++ b/pkg/machinery/config/types/block/block.go @@ -5,6 +5,6 @@ // Package block provides block device and volume configuration documents. package block -//go:generate go tool github.com/siderolabs/talos/tools/docgen -output block_doc.go block.go encryption.go existing_volume_config.go raw_volume_config.go swap_volume_config.go user_volume_config.go volume_config.go zswap_config.go +//go:generate go tool github.com/siderolabs/talos/tools/docgen -output block_doc.go block.go encryption.go existing_volume_config.go external_volume_config.go raw_volume_config.go swap_volume_config.go user_volume_config.go volume_config.go zswap_config.go -//go:generate go tool github.com/siderolabs/deep-copy -type ExistingVolumeConfigV1Alpha1 -type RawVolumeConfigV1Alpha1 -type SwapVolumeConfigV1Alpha1 -type UserVolumeConfigV1Alpha1 -type VolumeConfigV1Alpha1 -type ZswapConfigV1Alpha1 -pointer-receiver -header-file ../../../../../hack/boilerplate.txt -o deep_copy.generated.go . +//go:generate go tool github.com/siderolabs/deep-copy -type ExistingVolumeConfigV1Alpha1 -type RawVolumeConfigV1Alpha1 -type SwapVolumeConfigV1Alpha1 -type UserVolumeConfigV1Alpha1 -type ExternalVolumeConfigV1Alpha1 -type VolumeConfigV1Alpha1 -type ZswapConfigV1Alpha1 -pointer-receiver -header-file ../../../../../hack/boilerplate.txt -o deep_copy.generated.go . diff --git a/pkg/machinery/config/types/block/block_doc.go b/pkg/machinery/config/types/block/block_doc.go index 66ebbdea736..bb992f17cc7 100644 --- a/pkg/machinery/config/types/block/block_doc.go +++ b/pkg/machinery/config/types/block/block_doc.go @@ -404,6 +404,122 @@ func (MountSpec) Doc() *encoder.Doc { return doc } +func (ExternalVolumeConfigV1Alpha1) Doc() *encoder.Doc { + doc := &encoder.Doc{ + Type: "ExternalVolumeConfig", + Comments: [3]string{"" /* encoder.HeadComment */, "ExternalVolumeConfig is a external disk mount configuration document." /* encoder.LineComment */, "" /* encoder.FootComment */}, + Description: "ExternalVolumeConfig is a external disk mount configuration document.\nExternal volumes allow to mount volumes that were created outside of Talos,\nover the network or API. Volume will be mounted under `/var/mnt/`.\nThe external volume config name should not conflict with user volume names.\n", + Fields: []encoder.Doc{ + { + Type: "Meta", + Inline: true, + }, + { + Name: "name", + Type: "string", + Note: "", + Description: "Name of the mount.\n\nName might be between 1 and 34 characters long and can only contain:\nlowercase and uppercase ASCII letters, digits, and hyphens.", + Comments: [3]string{"" /* encoder.HeadComment */, "Name of the mount." /* encoder.LineComment */, "" /* encoder.FootComment */}, + }, + { + Name: "filesystemType", + Type: "FilesystemType", + Note: "", + Description: "Filesystem type.", + Comments: [3]string{"" /* encoder.HeadComment */, "Filesystem type." /* encoder.LineComment */, "" /* encoder.FootComment */}, + Values: []string{ + "virtiofs", + "nfs", + }, + }, + { + Name: "mount", + Type: "ExternalMountSpec", + Note: "", + Description: "The mount describes additional mount options.", + Comments: [3]string{"" /* encoder.HeadComment */, "The mount describes additional mount options." /* encoder.LineComment */, "" /* encoder.FootComment */}, + }, + }, + } + + doc.AddExample("", exampleExternalVolumeConfigV1Alpha1Virtiofs()) + + doc.AddExample("", exampleExternalVolumeConfigV1Alpha1NFS()) + + return doc +} + +func (ExternalMountSpec) Doc() *encoder.Doc { + doc := &encoder.Doc{ + Type: "ExternalMountSpec", + Comments: [3]string{"" /* encoder.HeadComment */, "ExternalMountSpec describes how the external volume is mounted." /* encoder.LineComment */, "" /* encoder.FootComment */}, + Description: "ExternalMountSpec describes how the external volume is mounted.", + AppearsIn: []encoder.Appearance{ + { + TypeName: "ExternalVolumeConfigV1Alpha1", + FieldName: "mount", + }, + }, + Fields: []encoder.Doc{ + { + Name: "readOnly", + Type: "bool", + Note: "", + Description: "Mount the volume read-only.", + Comments: [3]string{"" /* encoder.HeadComment */, "Mount the volume read-only." /* encoder.LineComment */, "" /* encoder.FootComment */}, + }, + { + Name: "source", + Type: "string", + Note: "", + Description: "Source of the volume.", + Comments: [3]string{"" /* encoder.HeadComment */, "Source of the volume." /* encoder.LineComment */, "" /* encoder.FootComment */}, + }, + { + Name: "nfs", + Type: "NFSMountSpec", + Note: "", + Description: "NFS mount options.", + Comments: [3]string{"" /* encoder.HeadComment */, "NFS mount options." /* encoder.LineComment */, "" /* encoder.FootComment */}, + }, + }, + } + + return doc +} + +func (NFSMountSpec) Doc() *encoder.Doc { + doc := &encoder.Doc{ + Type: "NFSMountSpec", + Comments: [3]string{"" /* encoder.HeadComment */, "NFSMountSpec describes NFS mount options." /* encoder.LineComment */, "" /* encoder.FootComment */}, + Description: "NFSMountSpec describes NFS mount options.", + AppearsIn: []encoder.Appearance{ + { + TypeName: "ExternalMountSpec", + FieldName: "nfs", + }, + }, + Fields: []encoder.Doc{ + { + Name: "version", + Type: "NFSVersionType", + Note: "", + Description: "NFS version to use.", + Comments: [3]string{"" /* encoder.HeadComment */, "NFS version to use." /* encoder.LineComment */, "" /* encoder.FootComment */}, + Values: []string{ + "4.2", + "4.1", + "4", + "3", + "2", + }, + }, + }, + } + + return doc +} + func (RawVolumeConfigV1Alpha1) Doc() *encoder.Doc { doc := &encoder.Doc{ Type: "RawVolumeConfig", @@ -758,6 +874,9 @@ func GetFileDoc() *encoder.FileDoc { VolumeDiscoverySpec{}.Doc(), VolumeSelector{}.Doc(), MountSpec{}.Doc(), + ExternalVolumeConfigV1Alpha1{}.Doc(), + ExternalMountSpec{}.Doc(), + NFSMountSpec{}.Doc(), RawVolumeConfigV1Alpha1{}.Doc(), SwapVolumeConfigV1Alpha1{}.Doc(), UserVolumeConfigV1Alpha1{}.Doc(), diff --git a/pkg/machinery/config/types/block/deep_copy.generated.go b/pkg/machinery/config/types/block/deep_copy.generated.go index 565c4181539..1ce790140b2 100644 --- a/pkg/machinery/config/types/block/deep_copy.generated.go +++ b/pkg/machinery/config/types/block/deep_copy.generated.go @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -// Code generated by "deep-copy -type ExistingVolumeConfigV1Alpha1 -type RawVolumeConfigV1Alpha1 -type SwapVolumeConfigV1Alpha1 -type UserVolumeConfigV1Alpha1 -type VolumeConfigV1Alpha1 -type ZswapConfigV1Alpha1 -pointer-receiver -header-file ../../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT. +// Code generated by "deep-copy -type ExistingVolumeConfigV1Alpha1 -type RawVolumeConfigV1Alpha1 -type SwapVolumeConfigV1Alpha1 -type UserVolumeConfigV1Alpha1 -type ExternalVolumeConfigV1Alpha1 -type VolumeConfigV1Alpha1 -type ZswapConfigV1Alpha1 -pointer-receiver -header-file ../../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT. package block @@ -276,6 +276,20 @@ func (o *UserVolumeConfigV1Alpha1) DeepCopy() *UserVolumeConfigV1Alpha1 { return &cp } +// DeepCopy generates a deep copy of *ExternalVolumeConfigV1Alpha1. +func (o *ExternalVolumeConfigV1Alpha1) DeepCopy() *ExternalVolumeConfigV1Alpha1 { + var cp ExternalVolumeConfigV1Alpha1 = *o + if o.MountSpec.MountReadOnly != nil { + cp.MountSpec.MountReadOnly = new(bool) + *cp.MountSpec.MountReadOnly = *o.MountSpec.MountReadOnly + } + if o.MountSpec.MountNFS != nil { + cp.MountSpec.MountNFS = new(NFSMountSpec) + *cp.MountSpec.MountNFS = *o.MountSpec.MountNFS + } + return &cp +} + // DeepCopy generates a deep copy of *VolumeConfigV1Alpha1. func (o *VolumeConfigV1Alpha1) DeepCopy() *VolumeConfigV1Alpha1 { var cp VolumeConfigV1Alpha1 = *o diff --git a/pkg/machinery/config/types/block/external_volume_config.go b/pkg/machinery/config/types/block/external_volume_config.go new file mode 100644 index 00000000000..db90a89ea02 --- /dev/null +++ b/pkg/machinery/config/types/block/external_volume_config.go @@ -0,0 +1,265 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package block + +//docgen:jsonschema + +import ( + "errors" + "fmt" + "strings" + + "github.com/siderolabs/gen/optional" + "github.com/siderolabs/go-pointer" + + "github.com/siderolabs/talos/pkg/machinery/config/config" + "github.com/siderolabs/talos/pkg/machinery/config/internal/registry" + "github.com/siderolabs/talos/pkg/machinery/config/types/meta" + "github.com/siderolabs/talos/pkg/machinery/config/validation" + "github.com/siderolabs/talos/pkg/machinery/constants" + "github.com/siderolabs/talos/pkg/machinery/resources/block" +) + +// ExternalVolumeConfig is a config document kind. +const ExternalVolumeConfigKind = "ExternalVolumeConfig" + +func init() { + registry.Register(ExternalVolumeConfigKind, func(version string) config.Document { + switch version { + case "v1alpha1": //nolint:goconst + return &ExternalVolumeConfigV1Alpha1{} + default: + return nil + } + }) +} + +// Check interfaces. +var ( + _ config.ExternalVolumeConfig = &ExternalVolumeConfigV1Alpha1{} + _ config.NamedDocument = &ExternalVolumeConfigV1Alpha1{} + _ config.Validator = &ExternalVolumeConfigV1Alpha1{} +) + +const maxExternalVolumeNameLength = constants.PartitionLabelLength - len(constants.ExternalVolumePrefix) + +// FilesystemType is an alias for block.FilesystemType. +type FilesystemType = block.FilesystemType + +// NFSVersionType is an alias for block.NFSVersionType. +type NFSVersionType = block.NFSVersionType + +// ExternalVolumeConfigV1Alpha1 is a external disk mount configuration document. +// +// description: | +// External volumes allow to mount volumes that were created outside of Talos, +// over the network or API. Volume will be mounted under `/var/mnt/`. +// The external volume config name should not conflict with user volume names. +// examples: +// - value: exampleExternalVolumeConfigV1Alpha1Virtiofs() +// - value: exampleExternalVolumeConfigV1Alpha1NFS() +// alias: ExternalVolumeConfig +// schemaRoot: true +// schemaMeta: v1alpha1/ExternalVolumeConfig +type ExternalVolumeConfigV1Alpha1 struct { + meta.Meta `yaml:",inline"` + + // description: | + // Name of the mount. + // + // Name might be between 1 and 34 characters long and can only contain: + // lowercase and uppercase ASCII letters, digits, and hyphens. + MetaName string `yaml:"name"` + // description: | + // Filesystem type. + // values: + // - virtiofs + // - nfs + // schema: + // type: string + FilesystemType FilesystemType `yaml:"filesystemType"` + // description: | + // The mount describes additional mount options. + MountSpec ExternalMountSpec `yaml:"mount,omitempty"` +} + +// ExternalMountSpec describes how the external volume is mounted. +type ExternalMountSpec struct { + // description: | + // Mount the volume read-only. + MountReadOnly *bool `yaml:"readOnly,omitempty"` + + // description: | + // Source of the volume. + MountSource string `yaml:"source"` + + // description: | + // NFS mount options. + MountNFS *NFSMountSpec `yaml:"nfs,omitempty"` +} + +// NOTE: to not forget mappings https://man7.org/linux/man-pages/man5/nfs.5.html + +// NFSMountSpec describes NFS mount options. +type NFSMountSpec struct { + // description: | + // NFS version to use. + // values: + // - 4.2 + // - 4.1 + // - 4 + // - 3 + // - 2 + // schema: + // type: string + NFSVersion NFSVersionType `yaml:"version"` +} + +// NewExternalVolumeConfigV1Alpha1 creates a new user mount config document. +func NewExternalVolumeConfigV1Alpha1() *ExternalVolumeConfigV1Alpha1 { + return &ExternalVolumeConfigV1Alpha1{ + Meta: meta.Meta{ + MetaKind: ExternalVolumeConfigKind, + MetaAPIVersion: "v1alpha1", + }, + } +} + +func exampleExternalVolumeConfigV1Alpha1Virtiofs() *ExternalVolumeConfigV1Alpha1 { + cfg := NewExternalVolumeConfigV1Alpha1() + cfg.MetaName = "mount1" + cfg.FilesystemType = block.FilesystemTypeVirtiofs + cfg.MountSpec.MountSource = "Data" + + return cfg +} + +func exampleExternalVolumeConfigV1Alpha1NFS() *ExternalVolumeConfigV1Alpha1 { + cfg := NewExternalVolumeConfigV1Alpha1() + cfg.MetaName = "mount1" + cfg.FilesystemType = block.FilesystemTypeVirtiofs + cfg.MountSpec.MountSource = "10.2.21.1:/backups" + cfg.MountSpec.MountNFS = &NFSMountSpec{ + NFSVersion: block.NFSVersionType4_2, + } + + return cfg +} + +// Name implements config.NamedDocument interface. +func (s *ExternalVolumeConfigV1Alpha1) Name() string { + return s.MetaName +} + +// Clone implements config.Document interface. +func (s *ExternalVolumeConfigV1Alpha1) Clone() config.Document { + return s.DeepCopy() +} + +// Validate implements config.Validator interface. +// +//nolint:gocyclo,dupl +func (s *ExternalVolumeConfigV1Alpha1) Validate(validation.RuntimeMode, ...validation.Option) ([]string, error) { + var ( + warnings []string + validationErrors error + ) + + if s.MetaName == "" { + validationErrors = errors.Join(validationErrors, errors.New("name is required")) + } + + if len(s.MetaName) < 1 || len(s.MetaName) > maxExternalVolumeNameLength { + validationErrors = errors.Join(validationErrors, fmt.Errorf("name must be between 1 and %d characters long", maxExternalVolumeNameLength)) + } + + if strings.ContainsFunc(s.MetaName, func(r rune) bool { + switch { + case r >= 'a' && r <= 'z': + return false + case r >= 'A' && r <= 'Z': + return false + case r >= '0' && r <= '9': + return false + case r == '-': + return false + default: // invalid symbol + return true + } + }) { + validationErrors = errors.Join(validationErrors, errors.New("name can only contain lowercase and uppercase ASCII letters, digits, and hyphens")) + } + + if s.MountSpec.MountSource == "" { + validationErrors = errors.Join(validationErrors, errors.New("mount source is required")) + } + + switch s.FilesystemType { + case block.FilesystemTypeNFS: + extraWarnings, extraErrors := s.MountSpec.MountNFS.Validate() + + warnings = append(warnings, extraWarnings...) + validationErrors = errors.Join(validationErrors, extraErrors) + + case block.FilesystemTypeVirtiofs: + // TODO: virtiofs validation + + case block.FilesystemTypeNone, block.FilesystemTypeXFS, block.FilesystemTypeVFAT, block.FilesystemTypeEXT4, block.FilesystemTypeISO9660, block.FilesystemTypeSwap: + fallthrough + + default: + validationErrors = errors.Join(validationErrors, fmt.Errorf("invalid filesystem type: %s", s.FilesystemType)) + } + + return warnings, validationErrors +} + +// ExternalVolumeConfigSignal is a signal for user mount config. +func (s *ExternalVolumeConfigV1Alpha1) ExternalVolumeConfigSignal() {} + +// Type implements config.ExternalVolumeConfig interface. +func (s *ExternalVolumeConfigV1Alpha1) Type() FilesystemType { + return s.FilesystemType +} + +// Mount implements config.ExternalVolumeConfig interface. +func (s *ExternalVolumeConfigV1Alpha1) Mount() config.ExternalMountConfig { + return s.MountSpec +} + +// ReadOnly implements config.VolumeMountConfig interface. +func (s ExternalMountSpec) ReadOnly() bool { + return pointer.SafeDeref(s.MountReadOnly) +} + +// Source implements config.VolumeMountConfig interface. +func (s ExternalMountSpec) Source() string { + return s.MountSource +} + +// NFS implements config.VolumeMountConfig interface. +func (s ExternalMountSpec) NFS() optional.Optional[config.NFSMountConfig] { + if s.MountNFS == nil { + return optional.None[config.NFSMountConfig]() + } + + return optional.Some[config.NFSMountConfig](*s.MountNFS) +} + +// Version implements config.NFSMountConfig interface. +func (s NFSMountSpec) Version() string { + return s.NFSVersion.String() +} + +// Validate implements config.Validator interface. +func (s *NFSMountSpec) Validate() ([]string, error) { + var validationErrors error + + if s == nil { + return nil, validationErrors + } + + return nil, validationErrors +} diff --git a/pkg/machinery/config/types/block/external_volume_config_test.go b/pkg/machinery/config/types/block/external_volume_config_test.go new file mode 100644 index 00000000000..df20a0f616b --- /dev/null +++ b/pkg/machinery/config/types/block/external_volume_config_test.go @@ -0,0 +1,194 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//nolint:dupl,goconst +package block_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/siderolabs/talos/pkg/machinery/config/configloader" + "github.com/siderolabs/talos/pkg/machinery/config/encoder" + "github.com/siderolabs/talos/pkg/machinery/config/types/block" + "github.com/siderolabs/talos/pkg/machinery/constants" + blockres "github.com/siderolabs/talos/pkg/machinery/resources/block" +) + +func TestExternalVolumeConfigMarshalUnmarshal(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + + filename string + cfg func(t *testing.T) *block.ExternalVolumeConfigV1Alpha1 + }{} { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + cfg := test.cfg(t) + + warnings, err := cfg.Validate(validationMode{}) + require.NoError(t, err) + require.Empty(t, warnings) + + marshaled, err := encoder.NewEncoder(cfg, encoder.WithComments(encoder.CommentsDisabled)).Encode() + require.NoError(t, err) + + t.Log(string(marshaled)) + + expectedMarshaled, err := os.ReadFile(filepath.Join("testdata", test.filename)) + require.NoError(t, err) + + assert.Equal(t, string(expectedMarshaled), string(marshaled)) + + provider, err := configloader.NewFromBytes(expectedMarshaled) + require.NoError(t, err) + + docs := provider.Documents() + require.Len(t, docs, 1) + + assert.Equal(t, cfg, docs[0]) + }) + } +} + +func TestExternalVolumeConfigValidate(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + + cfg func(t *testing.T) *block.ExternalVolumeConfigV1Alpha1 + + expectedErrors string + }{ + { + name: "no name", + + cfg: func(t *testing.T) *block.ExternalVolumeConfigV1Alpha1 { + c := block.NewExternalVolumeConfigV1Alpha1() + c.FilesystemType = blockres.FilesystemTypeVirtiofs + c.MountSpec.MountSource = "Data" + + return c + }, + + expectedErrors: "name is required\nname must be between 1 and 34 characters long", + }, + { + name: "invalid characters in name", + + cfg: func(t *testing.T) *block.ExternalVolumeConfigV1Alpha1 { + c := block.NewExternalVolumeConfigV1Alpha1() + c.MetaName = "some/name" + c.FilesystemType = blockres.FilesystemTypeVirtiofs + c.MountSpec.MountSource = "Data" + + return c + }, + + expectedErrors: "name can only contain lowercase and uppercase ASCII letters, digits, and hyphens", + }, + { + name: "no mount source", + + cfg: func(t *testing.T) *block.ExternalVolumeConfigV1Alpha1 { + c := block.NewExternalVolumeConfigV1Alpha1() + c.MetaName = constants.EphemeralPartitionLabel + c.FilesystemType = blockres.FilesystemTypeVirtiofs + + return c + }, + + expectedErrors: "mount source is required", + }, + { + name: "invalid type", + + cfg: func(t *testing.T) *block.ExternalVolumeConfigV1Alpha1 { + c := block.NewExternalVolumeConfigV1Alpha1() + c.MetaName = constants.EphemeralPartitionLabel + c.FilesystemType = blockres.FilesystemTypeEXT4 + c.MountSpec.MountSource = "Data" + + return c + }, + + expectedErrors: "invalid filesystem type: ext4", + }, + { + name: "empty type", + + cfg: func(t *testing.T) *block.ExternalVolumeConfigV1Alpha1 { + c := block.NewExternalVolumeConfigV1Alpha1() + c.MetaName = constants.EphemeralPartitionLabel + c.MountSpec.MountSource = "Data" + + return c + }, + + expectedErrors: "invalid filesystem type: none", + }, + { + name: "valid virtiofs", + + cfg: func(t *testing.T) *block.ExternalVolumeConfigV1Alpha1 { + c := block.NewExternalVolumeConfigV1Alpha1() + c.MetaName = constants.EphemeralPartitionLabel + c.FilesystemType = blockres.FilesystemTypeVirtiofs + c.MountSpec.MountSource = "Data" + + return c + }, + }, + { + name: "valid nfs", + + cfg: func(t *testing.T) *block.ExternalVolumeConfigV1Alpha1 { + c := block.NewExternalVolumeConfigV1Alpha1() + c.MetaName = constants.EphemeralPartitionLabel + c.FilesystemType = blockres.FilesystemTypeNFS + c.MountSpec.MountSource = "10.2.3.1:/data" + + return c + }, + }, + { + name: "valid nfs full", + + cfg: func(t *testing.T) *block.ExternalVolumeConfigV1Alpha1 { + c := block.NewExternalVolumeConfigV1Alpha1() + c.MetaName = constants.EphemeralPartitionLabel + c.FilesystemType = blockres.FilesystemTypeNFS + c.MountSpec.MountSource = "server.example.com:/data" + c.MountSpec.MountNFS = new(block.NFSMountSpec) + c.MountSpec.MountNFS.NFSVersion = blockres.NFSVersionType4 + + return c + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + cfg := test.cfg(t) + + _, err := cfg.Validate(validationMode{}) + + if test.expectedErrors == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + + assert.EqualError(t, err, test.expectedErrors) + } + }) + } +} diff --git a/pkg/machinery/constants/constants.go b/pkg/machinery/constants/constants.go index 2c9051355ca..7d8c5228504 100644 --- a/pkg/machinery/constants/constants.go +++ b/pkg/machinery/constants/constants.go @@ -1281,6 +1281,9 @@ const ( // UserVolumePrefix is the prefix for the user volumes. UserVolumePrefix = "u-" + // ExternalVolumePrefix is the prefix for the user volumes. + ExternalVolumePrefix = "x-" + // RawVolumePrefix is the prefix for the raw volumes. RawVolumePrefix = "r-" diff --git a/pkg/machinery/resources/block/block.go b/pkg/machinery/resources/block/block.go index 6e174f36aaa..298559b4950 100644 --- a/pkg/machinery/resources/block/block.go +++ b/pkg/machinery/resources/block/block.go @@ -21,7 +21,7 @@ import ( //go:generate go tool github.com/siderolabs/deep-copy -type DeviceSpec -type DiscoveredVolumeSpec -type DiscoveryRefreshRequestSpec -type DiscoveryRefreshStatusSpec -type DiskSpec -type MountRequestSpec -type MountStatusSpec -type SwapStatusSpec -type SymlinkSpec -type SystemDiskSpec -type UserDiskConfigStatusSpec -type VolumeConfigSpec -type VolumeLifecycleSpec -type VolumeMountRequestSpec -type VolumeMountStatusSpec -type VolumeStatusSpec -type ZswapStatusSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go . -//go:generate go tool github.com/dmarkham/enumer -type=VolumeType,VolumePhase,FilesystemType,EncryptionKeyType,EncryptionProviderType -linecomment -text +//go:generate go tool github.com/dmarkham/enumer -type=VolumeType,VolumePhase,FilesystemType,EncryptionKeyType,EncryptionProviderType,NFSVersionType -linecomment -text // NamespaceName contains configuration resources. const NamespaceName resource.Namespace = v1alpha1.NamespaceName @@ -41,6 +41,9 @@ const RawVolumeLabel = "talos.dev/raw-volume" // ExistingVolumeLabel is the label for existing volumes. const ExistingVolumeLabel = "talos.dev/existing-volume" +// ExternalVolumeLabel is the label for external volumes. +const ExternalVolumeLabel = "talos.dev/external-volume" + // SwapVolumeLabel is the label for swap volumes. const SwapVolumeLabel = "talos.dev/swap-volume" diff --git a/pkg/machinery/resources/block/filesystemtype.go b/pkg/machinery/resources/block/filesystemtype.go index 6db1c34e853..3fd980cac39 100644 --- a/pkg/machinery/resources/block/filesystemtype.go +++ b/pkg/machinery/resources/block/filesystemtype.go @@ -11,10 +11,12 @@ type FilesystemType int // //structprotogen:gen_enum const ( - FilesystemTypeNone FilesystemType = iota // none - FilesystemTypeXFS // xfs - FilesystemTypeVFAT // vfat - FilesystemTypeEXT4 // ext4 - FilesystemTypeISO9660 // iso9660 - FilesystemTypeSwap // swap + FilesystemTypeNone FilesystemType = iota // none + FilesystemTypeXFS // xfs + FilesystemTypeVFAT // vfat + FilesystemTypeEXT4 // ext4 + FilesystemTypeISO9660 // iso9660 + FilesystemTypeSwap // swapi + FilesystemTypeNFS // nfs + FilesystemTypeVirtiofs // virtiofs ) diff --git a/pkg/machinery/resources/block/nfsversiontype.go b/pkg/machinery/resources/block/nfsversiontype.go new file mode 100644 index 00000000000..7438e8bd190 --- /dev/null +++ b/pkg/machinery/resources/block/nfsversiontype.go @@ -0,0 +1,19 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package block + +// NFSVersionType describes NFS version type. +type NFSVersionType int + +// NFS Version types. +// +//structprotogen:gen_enum +const ( + NFSVersionType4_2 NFSVersionType = iota // 4.2 + NFSVersionType4_1 // 4.1 + NFSVersionType4 // 4 + NFSVersionType3 // 3 + NFSVersionType2 // 2 +) diff --git a/pkg/machinery/resources/block/volumetype.go b/pkg/machinery/resources/block/volumetype.go index c973330f4c8..02f53d55a1e 100644 --- a/pkg/machinery/resources/block/volumetype.go +++ b/pkg/machinery/resources/block/volumetype.go @@ -17,4 +17,5 @@ const ( VolumeTypeDirectory // directory VolumeTypeSymlink // symlink VolumeTypeOverlay // overlay + VolumeTypeExternal // external ) diff --git a/pkg/machinery/resources/block/volumetype_enumer.go b/pkg/machinery/resources/block/volumetype_enumer.go index 80868b9720e..d3b80887226 100644 --- a/pkg/machinery/resources/block/volumetype_enumer.go +++ b/pkg/machinery/resources/block/volumetype_enumer.go @@ -1,4 +1,4 @@ -// Code generated by "enumer -type=VolumeType,VolumePhase,FilesystemType,EncryptionKeyType,EncryptionProviderType -linecomment -text"; DO NOT EDIT. +// Code generated by "enumer -type=VolumeType,VolumePhase,FilesystemType,EncryptionKeyType,EncryptionProviderType,NFSVersionType -linecomment -text"; DO NOT EDIT. package block @@ -7,11 +7,11 @@ import ( "strings" ) -const _VolumeTypeName = "partitiondisktmpfsdirectorysymlinkoverlay" +const _VolumeTypeName = "partitiondisktmpfsdirectorysymlinkoverlayexternal" -var _VolumeTypeIndex = [...]uint8{0, 9, 13, 18, 27, 34, 41} +var _VolumeTypeIndex = [...]uint8{0, 9, 13, 18, 27, 34, 41, 49} -const _VolumeTypeLowerName = "partitiondisktmpfsdirectorysymlinkoverlay" +const _VolumeTypeLowerName = "partitiondisktmpfsdirectorysymlinkoverlayexternal" func (i VolumeType) String() string { if i < 0 || i >= VolumeType(len(_VolumeTypeIndex)-1) { @@ -30,9 +30,10 @@ func _VolumeTypeNoOp() { _ = x[VolumeTypeDirectory-(3)] _ = x[VolumeTypeSymlink-(4)] _ = x[VolumeTypeOverlay-(5)] + _ = x[VolumeTypeExternal-(6)] } -var _VolumeTypeValues = []VolumeType{VolumeTypePartition, VolumeTypeDisk, VolumeTypeTmpfs, VolumeTypeDirectory, VolumeTypeSymlink, VolumeTypeOverlay} +var _VolumeTypeValues = []VolumeType{VolumeTypePartition, VolumeTypeDisk, VolumeTypeTmpfs, VolumeTypeDirectory, VolumeTypeSymlink, VolumeTypeOverlay, VolumeTypeExternal} var _VolumeTypeNameToValueMap = map[string]VolumeType{ _VolumeTypeName[0:9]: VolumeTypePartition, @@ -47,6 +48,8 @@ var _VolumeTypeNameToValueMap = map[string]VolumeType{ _VolumeTypeLowerName[27:34]: VolumeTypeSymlink, _VolumeTypeName[34:41]: VolumeTypeOverlay, _VolumeTypeLowerName[34:41]: VolumeTypeOverlay, + _VolumeTypeName[41:49]: VolumeTypeExternal, + _VolumeTypeLowerName[41:49]: VolumeTypeExternal, } var _VolumeTypeNames = []string{ @@ -56,6 +59,7 @@ var _VolumeTypeNames = []string{ _VolumeTypeName[18:27], _VolumeTypeName[27:34], _VolumeTypeName[34:41], + _VolumeTypeName[41:49], } // VolumeTypeString retrieves an enum value from the enum constants string name. @@ -211,11 +215,11 @@ func (i *VolumePhase) UnmarshalText(text []byte) error { return err } -const _FilesystemTypeName = "nonexfsvfatext4iso9660swap" +const _FilesystemTypeName = "nonexfsvfatext4iso9660swapinfsvirtiofs" -var _FilesystemTypeIndex = [...]uint8{0, 4, 7, 11, 15, 22, 26} +var _FilesystemTypeIndex = [...]uint8{0, 4, 7, 11, 15, 22, 27, 30, 38} -const _FilesystemTypeLowerName = "nonexfsvfatext4iso9660swap" +const _FilesystemTypeLowerName = "nonexfsvfatext4iso9660swapinfsvirtiofs" func (i FilesystemType) String() string { if i < 0 || i >= FilesystemType(len(_FilesystemTypeIndex)-1) { @@ -234,9 +238,11 @@ func _FilesystemTypeNoOp() { _ = x[FilesystemTypeEXT4-(3)] _ = x[FilesystemTypeISO9660-(4)] _ = x[FilesystemTypeSwap-(5)] + _ = x[FilesystemTypeNFS-(6)] + _ = x[FilesystemTypeVirtiofs-(7)] } -var _FilesystemTypeValues = []FilesystemType{FilesystemTypeNone, FilesystemTypeXFS, FilesystemTypeVFAT, FilesystemTypeEXT4, FilesystemTypeISO9660, FilesystemTypeSwap} +var _FilesystemTypeValues = []FilesystemType{FilesystemTypeNone, FilesystemTypeXFS, FilesystemTypeVFAT, FilesystemTypeEXT4, FilesystemTypeISO9660, FilesystemTypeSwap, FilesystemTypeNFS, FilesystemTypeVirtiofs} var _FilesystemTypeNameToValueMap = map[string]FilesystemType{ _FilesystemTypeName[0:4]: FilesystemTypeNone, @@ -249,8 +255,12 @@ var _FilesystemTypeNameToValueMap = map[string]FilesystemType{ _FilesystemTypeLowerName[11:15]: FilesystemTypeEXT4, _FilesystemTypeName[15:22]: FilesystemTypeISO9660, _FilesystemTypeLowerName[15:22]: FilesystemTypeISO9660, - _FilesystemTypeName[22:26]: FilesystemTypeSwap, - _FilesystemTypeLowerName[22:26]: FilesystemTypeSwap, + _FilesystemTypeName[22:27]: FilesystemTypeSwap, + _FilesystemTypeLowerName[22:27]: FilesystemTypeSwap, + _FilesystemTypeName[27:30]: FilesystemTypeNFS, + _FilesystemTypeLowerName[27:30]: FilesystemTypeNFS, + _FilesystemTypeName[30:38]: FilesystemTypeVirtiofs, + _FilesystemTypeLowerName[30:38]: FilesystemTypeVirtiofs, } var _FilesystemTypeNames = []string{ @@ -259,7 +269,9 @@ var _FilesystemTypeNames = []string{ _FilesystemTypeName[7:11], _FilesystemTypeName[11:15], _FilesystemTypeName[15:22], - _FilesystemTypeName[22:26], + _FilesystemTypeName[22:27], + _FilesystemTypeName[27:30], + _FilesystemTypeName[30:38], } // FilesystemTypeString retrieves an enum value from the enum constants string name. @@ -480,3 +492,97 @@ func (i *EncryptionProviderType) UnmarshalText(text []byte) error { *i, err = EncryptionProviderTypeString(string(text)) return err } + +const _NFSVersionTypeName = "4.24.1432" + +var _NFSVersionTypeIndex = [...]uint8{0, 3, 6, 7, 8, 9} + +const _NFSVersionTypeLowerName = "4.24.1432" + +func (i NFSVersionType) String() string { + if i < 0 || i >= NFSVersionType(len(_NFSVersionTypeIndex)-1) { + return fmt.Sprintf("NFSVersionType(%d)", i) + } + return _NFSVersionTypeName[_NFSVersionTypeIndex[i]:_NFSVersionTypeIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _NFSVersionTypeNoOp() { + var x [1]struct{} + _ = x[NFSVersionType4_2-(0)] + _ = x[NFSVersionType4_1-(1)] + _ = x[NFSVersionType4-(2)] + _ = x[NFSVersionType3-(3)] + _ = x[NFSVersionType2-(4)] +} + +var _NFSVersionTypeValues = []NFSVersionType{NFSVersionType4_2, NFSVersionType4_1, NFSVersionType4, NFSVersionType3, NFSVersionType2} + +var _NFSVersionTypeNameToValueMap = map[string]NFSVersionType{ + _NFSVersionTypeName[0:3]: NFSVersionType4_2, + _NFSVersionTypeLowerName[0:3]: NFSVersionType4_2, + _NFSVersionTypeName[3:6]: NFSVersionType4_1, + _NFSVersionTypeLowerName[3:6]: NFSVersionType4_1, + _NFSVersionTypeName[6:7]: NFSVersionType4, + _NFSVersionTypeLowerName[6:7]: NFSVersionType4, + _NFSVersionTypeName[7:8]: NFSVersionType3, + _NFSVersionTypeLowerName[7:8]: NFSVersionType3, + _NFSVersionTypeName[8:9]: NFSVersionType2, + _NFSVersionTypeLowerName[8:9]: NFSVersionType2, +} + +var _NFSVersionTypeNames = []string{ + _NFSVersionTypeName[0:3], + _NFSVersionTypeName[3:6], + _NFSVersionTypeName[6:7], + _NFSVersionTypeName[7:8], + _NFSVersionTypeName[8:9], +} + +// NFSVersionTypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func NFSVersionTypeString(s string) (NFSVersionType, error) { + if val, ok := _NFSVersionTypeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _NFSVersionTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to NFSVersionType values", s) +} + +// NFSVersionTypeValues returns all values of the enum +func NFSVersionTypeValues() []NFSVersionType { + return _NFSVersionTypeValues +} + +// NFSVersionTypeStrings returns a slice of all String values of the enum +func NFSVersionTypeStrings() []string { + strs := make([]string, len(_NFSVersionTypeNames)) + copy(strs, _NFSVersionTypeNames) + return strs +} + +// IsANFSVersionType returns "true" if the value is listed in the enum definition. "false" otherwise +func (i NFSVersionType) IsANFSVersionType() bool { + for _, v := range _NFSVersionTypeValues { + if i == v { + return true + } + } + return false +} + +// MarshalText implements the encoding.TextMarshaler interface for NFSVersionType +func (i NFSVersionType) MarshalText() ([]byte, error) { + return []byte(i.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for NFSVersionType +func (i *NFSVersionType) UnmarshalText(text []byte) error { + var err error + *i, err = NFSVersionTypeString(string(text)) + return err +} diff --git a/pkg/provision/providers/qemu/create.go b/pkg/provision/providers/qemu/create.go index 3a52f688dfc..a8d0a42b44e 100644 --- a/pkg/provision/providers/qemu/create.go +++ b/pkg/provision/providers/qemu/create.go @@ -75,6 +75,27 @@ func (p *provisioner) Create(ctx context.Context, request provision.ClusterReque return nil, fmt.Errorf("error creating dnsd: %w", err) } + fmt.Fprintln(options.LogWriter, "creating nfsd") + + if err = p.CreateNFSd(state, request); err != nil { + return nil, fmt.Errorf("error creating nfsd: %w", err) + } + + if false { + virtiofsdBin, err := p.FindVirtiofsd() + if err != nil { + return nil, fmt.Errorf("virtiofsd lookup: %w", err) + } + + if virtiofsdBin != "" { + fmt.Fprintln(options.LogWriter, "creating virtiofsd") + + if err = p.CreateVirtiofsd(state, request, virtiofsdBin); err != nil { + return nil, fmt.Errorf("error creating virtiofsd: %w", err) + } + } + } + if request.Network.ImageCachePath != "" { fmt.Fprintln(options.LogWriter, "creating image cache") diff --git a/pkg/provision/providers/qemu/destroy.go b/pkg/provision/providers/qemu/destroy.go index 97dc52c7138..3ea07932277 100644 --- a/pkg/provision/providers/qemu/destroy.go +++ b/pkg/provision/providers/qemu/destroy.go @@ -15,7 +15,7 @@ import ( // Destroy Talos cluster as set of qemu VMs. // -//nolint:gocyclo +//nolint:gocyclo,cyclop func (p *provisioner) Destroy(ctx context.Context, cluster provision.Cluster, opts ...provision.Option) error { options := provision.DefaultOptions() @@ -76,6 +76,18 @@ func (p *provisioner) Destroy(ctx context.Context, cluster provision.Cluster, op return fmt.Errorf("error stopping image cache: %w", err) } + fmt.Fprintln(options.LogWriter, "removing virtiofsd") + + if err := p.DestroyNFSd(state); err != nil { + return fmt.Errorf("error stopping virtiofsd: %w", err) + } + + fmt.Fprintln(options.LogWriter, "removing nfsd") + + if err := p.DestroyNFSd(state); err != nil { + return fmt.Errorf("error stopping dnsd: %w", err) + } + fmt.Fprintln(options.LogWriter, "removing dnsd") if err := p.DestroyDNSd(state); err != nil { diff --git a/pkg/provision/providers/vm/nfsd.go b/pkg/provision/providers/vm/nfsd.go new file mode 100644 index 00000000000..10540ad4e3b --- /dev/null +++ b/pkg/provision/providers/vm/nfsd.go @@ -0,0 +1,116 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package vm + +import ( + "fmt" + "net" + "net/netip" + "os" + "os/exec" + "strconv" + "strings" + "syscall" + + "github.com/siderolabs/gen/xslices" + "github.com/smallfz/libnfs-go/auth" + "github.com/smallfz/libnfs-go/backend" + "github.com/smallfz/libnfs-go/fs" + "github.com/smallfz/libnfs-go/server" + "github.com/smallfz/libnfs-go/unixfs" + "golang.org/x/sync/errgroup" + + "github.com/siderolabs/talos/pkg/provision" +) + +const ( + nfsdPid = "nfsd.pid" + nfsdLog = "nfsd.log" + nfsdWorkdir = "nfsd.d" +) + +// NFSd starts a userspace NFS server on the given IPs. +func NFSd(ips []net.IP, workdir string) error { + var eg errgroup.Group + + if workdir == "" { + return fmt.Errorf("workdir must be specified") + } + + f, err := unixfs.New(workdir) + if err != nil { + return fmt.Errorf("failed to create unixfs: %w", err) + } + + for _, ip := range ips { + eg.Go(func() error { + srv, err := server.NewServerTCP(net.JoinHostPort(ip.String(), "2049"), backend.New( + func() fs.FS { return f }, + auth.Null, + )) + if err != nil { + return fmt.Errorf("failed to create TCP server: %w", err) + } + + return srv.Serve() + }) + } + + return eg.Wait() +} + +// CreateNFSd creates the NFSd server. +func (p *Provisioner) CreateNFSd(state *State, clusterReq provision.ClusterRequest) error { + pidPath := state.GetRelativePath(nfsdPid) + + logFile, err := os.OpenFile(state.GetRelativePath(nfsdLog), os.O_APPEND|os.O_CREATE|os.O_RDWR, 0o666) + if err != nil { + return err + } + + defer logFile.Close() //nolint:errcheck + + nfsdPath := state.GetRelativePath(nfsdWorkdir) + + if err = os.MkdirAll(nfsdPath, 0o755); err != nil { + return fmt.Errorf("error creating nfsd workdir: %w", err) + } + + defer func() { + os.RemoveAll(nfsdPath) //nolint:errcheck + }() + + gatewayAddrs := xslices.Map(clusterReq.Network.GatewayAddrs, netip.Addr.String) + + args := []string{ + "nfsd-launch", + "--addr", strings.Join(gatewayAddrs, ","), + "--workdir", nfsdPath, + } + + cmd := exec.Command(clusterReq.SelfExecutable, args...) //nolint:noctx // runs in background + cmd.Stdout = logFile + cmd.Stderr = logFile + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, // daemonize + } + + if err = cmd.Start(); err != nil { + return err + } + + if err = os.WriteFile(pidPath, []byte(strconv.Itoa(cmd.Process.Pid)), os.ModePerm); err != nil { + return fmt.Errorf("error writing nfsd PID file: %w", err) + } + + return nil +} + +// DestroyNFSd destoys NFSd server. +func (p *Provisioner) DestroyNFSd(state *State) error { + pidPath := state.GetRelativePath(nfsdPid) + + return StopProcessByPidfile(pidPath) +} diff --git a/pkg/provision/providers/vm/virtiofsd.go b/pkg/provision/providers/vm/virtiofsd.go new file mode 100644 index 00000000000..a6d21ab5649 --- /dev/null +++ b/pkg/provision/providers/vm/virtiofsd.go @@ -0,0 +1,54 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package vm + +import ( + "context" + "errors" + "os" + "os/exec" + + "github.com/siderolabs/talos/pkg/provision" +) + +const ( + virtiofsdPid = "virtiofsd.pid" + virtiofsdLog = "virtiofsd.log" +) + +// FindVirtiofsd tries to find the virtiofsd binary in common locations. +func (p *Provisioner) FindVirtiofsd() (string, error) { + return p.findVirtiofsd() +} + +// Virtiofsd starts the Virtiofsd server. +func Virtiofsd(ctx context.Context, virtiofsdBin, share, socket string) error { + if virtiofsdBin == "" { + return errors.New("virtiofsd binary path is empty") + } + + args := []string{ + "--shared-dir", share, + "--socket-path", socket, + "--announce-submounts", + "--inode-file-handles", "mandatory", + } + + cmd := exec.CommandContext(ctx, virtiofsdBin, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} + +// CreateVirtiofsd creates the Virtiofsd server. +func (p *Provisioner) CreateVirtiofsd(state *State, clusterReq provision.ClusterRequest, virtiofdPath string) error { + return p.startVirtiofsd(state, clusterReq, virtiofdPath) +} + +// DestroyVirtiofsd destoys Virtiofsd server. +func (p *Provisioner) DestroyVirtiofsd(state *State) error { + return p.stopVirtiofsd(state) +} diff --git a/pkg/provision/providers/vm/virtiofsd_darwin.go b/pkg/provision/providers/vm/virtiofsd_darwin.go new file mode 100644 index 00000000000..83e1dcde5d1 --- /dev/null +++ b/pkg/provision/providers/vm/virtiofsd_darwin.go @@ -0,0 +1,21 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package vm + +import ( + "github.com/siderolabs/talos/pkg/provision" +) + +func (p *Provisioner) findVirtiofsd() (string, error) { + return "", nil +} + +func (p *Provisioner) startVirtiofsd(_ *State, _ provision.ClusterRequest, _ string) error { + return nil +} + +func (p *Provisioner) stopVirtiofsd(_ *State) error { + return nil +} diff --git a/pkg/provision/providers/vm/virtiofsd_linux.go b/pkg/provision/providers/vm/virtiofsd_linux.go new file mode 100644 index 00000000000..cd4eed8e09b --- /dev/null +++ b/pkg/provision/providers/vm/virtiofsd_linux.go @@ -0,0 +1,70 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package vm + +import ( + "fmt" + "os" + "os/exec" + "strconv" + "syscall" + + "github.com/siderolabs/talos/pkg/provision" +) + +func (p *Provisioner) findVirtiofsd() (string, error) { + virtiofsdPaths := []string{ + "virtiofsd", + "/usr/libexec/virtiofsd", + } + + for _, p := range virtiofsdPaths { + if full, err := exec.LookPath(p); err == nil { + return full, nil + } + } + + return "", fmt.Errorf("virtiofsd not found in paths: %v", virtiofsdPaths) +} + +func (p *Provisioner) startVirtiofsd(state *State, clusterReq provision.ClusterRequest, virtiofdPath string) error { + pidPath := state.GetRelativePath(virtiofsdPid) + + logFile, err := os.OpenFile(state.GetRelativePath(virtiofsdLog), os.O_APPEND|os.O_CREATE|os.O_RDWR, 0o666) + if err != nil { + return err + } + + defer logFile.Close() //nolint:errcheck + + args := []string{ + "virtiofsd-launch", + "--bin", virtiofdPath, + // TODO: add virtiofd configs + } + + cmd := exec.Command(clusterReq.SelfExecutable, args...) //nolint:noctx // runs in background + cmd.Stdout = logFile + cmd.Stderr = logFile + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, // daemonize + } + + if err = cmd.Start(); err != nil { + return err + } + + if err = os.WriteFile(pidPath, []byte(strconv.Itoa(cmd.Process.Pid)), os.ModePerm); err != nil { + return fmt.Errorf("error writing virtiofsd PID file: %w", err) + } + + return nil +} + +func (p *Provisioner) stopVirtiofsd(state *State) error { + pidPath := state.GetRelativePath(virtiofsdPid) + + return StopProcessByPidfile(pidPath) +} diff --git a/website/content/v1.12/reference/api.md b/website/content/v1.12/reference/api.md index a2864ff92ed..43e5a0da6ee 100644 --- a/website/content/v1.12/reference/api.md +++ b/website/content/v1.12/reference/api.md @@ -253,6 +253,7 @@ description: Talos gRPC API reference. - [BlockEncryptionKeyType](#talos.resource.definitions.enums.BlockEncryptionKeyType) - [BlockEncryptionProviderType](#talos.resource.definitions.enums.BlockEncryptionProviderType) - [BlockFilesystemType](#talos.resource.definitions.enums.BlockFilesystemType) + - [BlockNFSVersionType](#talos.resource.definitions.enums.BlockNFSVersionType) - [BlockVolumePhase](#talos.resource.definitions.enums.BlockVolumePhase) - [BlockVolumeType](#talos.resource.definitions.enums.BlockVolumeType) - [CriImageCacheCopyStatus](#talos.resource.definitions.enums.CriImageCacheCopyStatus) @@ -4507,6 +4508,23 @@ BlockFilesystemType describes filesystem type. | FILESYSTEM_TYPE_EXT4 | 3 | | | FILESYSTEM_TYPE_ISO9660 | 4 | | | FILESYSTEM_TYPE_SWAP | 5 | | +| FILESYSTEM_TYPE_NFS | 6 | | +| FILESYSTEM_TYPE_VIRTIOFS | 7 | | + + + + + +### BlockNFSVersionType +BlockNFSVersionType describes NFS version type. + +| Name | Number | Description | +| ---- | ------ | ----------- | +| NFS_VERSION_TYPE4_2 | 0 | | +| NFS_VERSION_TYPE4_1 | 1 | | +| NFS_VERSION_TYPE4 | 2 | | +| NFS_VERSION_TYPE3 | 3 | | +| NFS_VERSION_TYPE2 | 4 | | @@ -4541,6 +4559,7 @@ BlockVolumeType describes volume type. | VOLUME_TYPE_DIRECTORY | 3 | | | VOLUME_TYPE_SYMLINK | 4 | | | VOLUME_TYPE_OVERLAY | 5 | | +| VOLUME_TYPE_EXTERNAL | 6 | | diff --git a/website/content/v1.12/reference/configuration/block/externalvolumeconfig.md b/website/content/v1.12/reference/configuration/block/externalvolumeconfig.md new file mode 100644 index 00000000000..2d4e3242fc7 --- /dev/null +++ b/website/content/v1.12/reference/configuration/block/externalvolumeconfig.md @@ -0,0 +1,88 @@ +--- +description: | + ExternalVolumeConfig is a external disk mount configuration document. + External volumes allow to mount volumes that were created outside of Talos, + over the network or API. Volume will be mounted under `/var/mnt/`. + The external volume config name should not conflict with user volume names. +title: ExternalVolumeConfig +--- + + + + + + + + + + + +{{< highlight yaml >}} +apiVersion: v1alpha1 +kind: ExternalVolumeConfig +name: mount1 # Name of the mount. +filesystemType: virtiofs # Filesystem type. +# The mount describes additional mount options. +mount: + source: Data # Source of the volume. +{{< /highlight >}} + +{{< highlight yaml >}} +apiVersion: v1alpha1 +kind: ExternalVolumeConfig +name: mount1 # Name of the mount. +filesystemType: virtiofs # Filesystem type. +# The mount describes additional mount options. +mount: + source: 10.2.21.1:/backups # Source of the volume. + # NFS mount options. + nfs: + version: "4.2" # NFS version to use. +{{< /highlight >}} + + +| Field | Type | Description | Value(s) | +|-------|------|-------------|----------| +|`name` |string |Name of the mount.

Name might be between 1 and 34 characters long and can only contain:
lowercase and uppercase ASCII letters, digits, and hyphens. | | +|`filesystemType` |FilesystemType |Filesystem type. |`virtiofs`
`nfs`
| +|`mount` |ExternalMountSpec |The mount describes additional mount options. | | + + + + +## mount {#ExternalVolumeConfig.mount} + +ExternalMountSpec describes how the external volume is mounted. + + + + +| Field | Type | Description | Value(s) | +|-------|------|-------------|----------| +|`readOnly` |bool |Mount the volume read-only. | | +|`source` |string |Source of the volume. | | +|`nfs` |NFSMountSpec |NFS mount options. | | + + + + +### nfs {#ExternalVolumeConfig.mount.nfs} + +NFSMountSpec describes NFS mount options. + + + + +| Field | Type | Description | Value(s) | +|-------|------|-------------|----------| +|`version` |NFSVersionType |NFS version to use. |`4.2`
`4.1`
`4`
`3`
`2`
| + + + + + + + + + + diff --git a/website/content/v1.12/schemas/config.schema.json b/website/content/v1.12/schemas/config.schema.json index 25586cf48b3..2580cce1819 100644 --- a/website/content/v1.12/schemas/config.schema.json +++ b/website/content/v1.12/schemas/config.schema.json @@ -249,6 +249,87 @@ ], "description": "ExistingVolumeConfig is an existing volume configuration document.\\nExisting volumes allow to mount partitions (or whole disks) that were created\\noutside of Talos. Volume will be mounted under `/var/mnt/\u003cname\u003e`.\\nThe existing volume config name should not conflict with user volume names.\\n" }, + "block.ExternalMountSpec": { + "properties": { + "readOnly": { + "type": "boolean", + "title": "readOnly", + "description": "Mount the volume read-only.\n", + "markdownDescription": "Mount the volume read-only.", + "x-intellij-html-description": "\u003cp\u003eMount the volume read-only.\u003c/p\u003e\n" + }, + "source": { + "type": "string", + "title": "source", + "description": "Source of the volume.\n", + "markdownDescription": "Source of the volume.", + "x-intellij-html-description": "\u003cp\u003eSource of the volume.\u003c/p\u003e\n" + }, + "nfs": { + "$ref": "#/$defs/block.NFSMountSpec", + "title": "nfs", + "description": "NFS mount options.\n", + "markdownDescription": "NFS mount options.", + "x-intellij-html-description": "\u003cp\u003eNFS mount options.\u003c/p\u003e\n" + } + }, + "additionalProperties": false, + "type": "object", + "description": "ExternalMountSpec describes how the external volume is mounted." + }, + "block.ExternalVolumeConfigV1Alpha1": { + "properties": { + "apiVersion": { + "enum": [ + "v1alpha1" + ], + "title": "apiVersion", + "description": "apiVersion is the API version of the resource.\n", + "markdownDescription": "apiVersion is the API version of the resource.", + "x-intellij-html-description": "\u003cp\u003eapiVersion is the API version of the resource.\u003c/p\u003e\n" + }, + "kind": { + "enum": [ + "ExternalVolumeConfig" + ], + "title": "kind", + "description": "kind is the kind of the resource.\n", + "markdownDescription": "kind is the kind of the resource.", + "x-intellij-html-description": "\u003cp\u003ekind is the kind of the resource.\u003c/p\u003e\n" + }, + "name": { + "type": "string", + "title": "name", + "description": "Name of the mount.\n\nName might be between 1 and 34 characters long and can only contain:\nlowercase and uppercase ASCII letters, digits, and hyphens.\n", + "markdownDescription": "Name of the mount.\n\nName might be between 1 and 34 characters long and can only contain:\nlowercase and uppercase ASCII letters, digits, and hyphens.", + "x-intellij-html-description": "\u003cp\u003eName of the mount.\u003c/p\u003e\n\n\u003cp\u003eName might be between 1 and 34 characters long and can only contain:\nlowercase and uppercase ASCII letters, digits, and hyphens.\u003c/p\u003e\n" + }, + "filesystemType": { + "enum": [ + "virtiofs", + "nfs" + ], + "title": "filesystemType", + "description": "Filesystem type.\n", + "markdownDescription": "Filesystem type.", + "x-intellij-html-description": "\u003cp\u003eFilesystem type.\u003c/p\u003e\n" + }, + "mount": { + "$ref": "#/$defs/block.ExternalMountSpec", + "title": "mount", + "description": "The mount describes additional mount options.\n", + "markdownDescription": "The mount describes additional mount options.", + "x-intellij-html-description": "\u003cp\u003eThe mount describes additional mount options.\u003c/p\u003e\n" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "apiVersion", + "kind" + ], + "description": "ExternalVolumeConfig is a external disk mount configuration document.\\nExternal volumes allow to mount volumes that were created outside of Talos,\\nover the network or API. Volume will be mounted under `/var/mnt/\u003cname\u003e`.\\nThe external volume config name should not conflict with user volume names.\\n" + }, "block.FilesystemSpec": { "properties": { "type": { @@ -287,6 +368,26 @@ "type": "object", "description": "MountSpec describes how the volume is mounted." }, + "block.NFSMountSpec": { + "properties": { + "version": { + "enum": [ + "4.2", + "4.1", + "4", + "3", + "2" + ], + "title": "version", + "description": "NFS version to use.\n", + "markdownDescription": "NFS version to use.", + "x-intellij-html-description": "\u003cp\u003eNFS version to use.\u003c/p\u003e\n" + } + }, + "additionalProperties": false, + "type": "object", + "description": "NFSMountSpec describes NFS mount options." + }, "block.ProvisioningSpec": { "properties": { "diskSelector": { @@ -5128,6 +5229,9 @@ { "$ref": "#/$defs/block.ExistingVolumeConfigV1Alpha1" }, + { + "$ref": "#/$defs/block.ExternalVolumeConfigV1Alpha1" + }, { "$ref": "#/$defs/block.RawVolumeConfigV1Alpha1" },