diff --git a/api/resource/definitions/block/block.proto b/api/resource/definitions/block/block.proto index 1a774d10439..10ef0b4659a 100755 --- a/api/resource/definitions/block/block.proto +++ b/api/resource/definitions/block/block.proto @@ -243,3 +243,17 @@ message VolumeStatusSpec { string parent_id = 19; } +// ZswapStatusSpec is the spec for ZswapStatus resource. +message ZswapStatusSpec { + uint64 total_size_bytes = 1; + string total_size_human = 2; + uint64 stored_pages = 3; + uint64 pool_limit_hit = 4; + uint64 reject_reclaim_fail = 5; + uint64 reject_alloc_fail = 6; + uint64 reject_kmemcache_fail = 7; + uint64 reject_compress_fail = 8; + uint64 reject_compress_poor = 9; + uint64 written_back_pages = 10; +} + diff --git a/cmd/talosctl/cmd/talos/cgroupsprinter/presets/swap.yaml b/cmd/talosctl/cmd/talos/cgroupsprinter/presets/swap.yaml index 6ebcded28fc..7133299feda 100644 --- a/cmd/talosctl/cmd/talos/cgroupsprinter/presets/swap.yaml +++ b/cmd/talosctl/cmd/talos/cgroupsprinter/presets/swap.yaml @@ -7,3 +7,9 @@ columns: template: '{{ .MemorySwapHigh.HumanizeIBytes | printf "%8s" }}' - name: SwapMax template: '{{ .MemorySwapMax.HumanizeIBytes | printf "%8s" }}' + - name: ZswapCurrent + template: '{{ .MemoryZswapCurrent.HumanizeIBytes | printf "%8s" }}' + - name: ZswapMax + template: '{{ .MemoryZswapMax.HumanizeIBytes | printf "%8s" }}' + - name: ZswapWriteback + template: '{{ .MemoryZswapWriteback }}' diff --git a/hack/release.toml b/hack/release.toml index b82bb8a6288..fad736ee00e 100644 --- a/hack/release.toml +++ b/hack/release.toml @@ -52,6 +52,13 @@ This feature can be enable by using [SwapVolumeConfig](https://www.talos.dev/v1. title = "VMware" description = """\ Talos VMWare platform now supports `arm64` architecture in addition to `amd64`. +""" + + [notes.zswap] + title = "Zswap Support" + description = """\ +Talos now supports zswap, a compressed cache for swap pages. +This feature can be enabled by using [ZswapConfig](https://www.talos.dev/v1.11/reference/configuration/block/zswapconfig/) document in the machine configuration. """ [make_deps] diff --git a/hack/test/patches/ephemeral-nvme.yaml b/hack/test/patches/ephemeral-nvme.yaml index e0df1a7262d..8eb4c385e05 100644 --- a/hack/test/patches/ephemeral-nvme.yaml +++ b/hack/test/patches/ephemeral-nvme.yaml @@ -21,3 +21,8 @@ machine: extraConfig: memorySwap: swapBehavior: LimitedSwap +--- +apiVersion: v1alpha1 +kind: ZswapConfig +maxPoolPercent: 25 +shrinkerEnabled: true diff --git a/internal/app/machined/pkg/controllers/block/zswap_config.go b/internal/app/machined/pkg/controllers/block/zswap_config.go new file mode 100644 index 00000000000..91eec9f60d8 --- /dev/null +++ b/internal/app/machined/pkg/controllers/block/zswap_config.go @@ -0,0 +1,115 @@ +// 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 + +import ( + "context" + "fmt" + "strconv" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/safe" + "github.com/cosi-project/runtime/pkg/state" + "github.com/siderolabs/gen/optional" + "go.uber.org/zap" + + configconfig "github.com/siderolabs/talos/pkg/machinery/config/config" + "github.com/siderolabs/talos/pkg/machinery/resources/config" + "github.com/siderolabs/talos/pkg/machinery/resources/runtime" +) + +// ZswapConfigController provides zswap configuration based machine configuration. +type ZswapConfigController struct{} + +// Name implements controller.Controller interface. +func (ctrl *ZswapConfigController) Name() string { + return "block.ZswapConfigController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *ZswapConfigController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: config.NamespaceName, + Type: config.MachineConfigType, + ID: optional.Some(config.ActiveID), + Kind: controller.InputWeak, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *ZswapConfigController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: runtime.KernelParamSpecType, + Kind: controller.OutputShared, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *ZswapConfigController) Run(ctx context.Context, r controller.Runtime, _ *zap.Logger) error { + for { + select { + case <-r.EventCh(): + case <-ctx.Done(): + return nil + } + + // load config if present + cfg, err := safe.ReaderGetByID[*config.MachineConfig](ctx, r, config.ActiveID) + if err != nil && !state.IsNotFoundError(err) { + return fmt.Errorf("error fetching machine configuration") + } + + r.StartTrackingOutputs() + + var zswapCfg configconfig.ZswapConfig + + if cfg != nil { + zswapCfg = cfg.Config().ZswapConfig() + } + + if zswapCfg != nil { // enabled + if err := safe.WriterModify(ctx, r, runtime.NewKernelParamSpec(runtime.NamespaceName, "sys.module.zswap.parameters.enabled"), + func(p *runtime.KernelParamSpec) error { + p.TypedSpec().Value = "Y" + + return nil + }); err != nil { + return fmt.Errorf("error setting zswap config: %w", err) + } + + if err := safe.WriterModify(ctx, r, runtime.NewKernelParamSpec(runtime.NamespaceName, "sys.module.zswap.parameters.max_pool_percent"), + func(p *runtime.KernelParamSpec) error { + p.TypedSpec().Value = strconv.Itoa(zswapCfg.MaxPoolPercent()) + + return nil + }); err != nil { + return fmt.Errorf("error setting zswap config: %w", err) + } + + if err := safe.WriterModify(ctx, r, runtime.NewKernelParamSpec(runtime.NamespaceName, "sys.module.zswap.parameters.shrinker_enabled"), + func(p *runtime.KernelParamSpec) error { + if zswapCfg.ShrinkerEnabled() { + p.TypedSpec().Value = "Y" + } else { + p.TypedSpec().Value = "N" + } + + return nil + }); err != nil { + return fmt.Errorf("error setting zswap config: %w", err) + } + } + + if err = safe.CleanupOutputs[*runtime.KernelParamSpec](ctx, r); err != nil { + return fmt.Errorf("error cleaning up volume configuration: %w", err) + } + } +} diff --git a/internal/app/machined/pkg/controllers/block/zswap_status.go b/internal/app/machined/pkg/controllers/block/zswap_status.go new file mode 100644 index 00000000000..aba1e37a6e2 --- /dev/null +++ b/internal/app/machined/pkg/controllers/block/zswap_status.go @@ -0,0 +1,131 @@ +// 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 + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "strconv" + "time" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/safe" + "github.com/dustin/go-humanize" + "go.uber.org/zap" + + machineruntime "github.com/siderolabs/talos/internal/app/machined/pkg/runtime" + "github.com/siderolabs/talos/pkg/machinery/resources/block" + "github.com/siderolabs/talos/pkg/machinery/resources/runtime" +) + +// ZswapStatusController provides a view of active swap devices. +type ZswapStatusController struct { + V1Alpha1Mode machineruntime.Mode +} + +// Name implements controller.Controller interface. +func (ctrl *ZswapStatusController) Name() string { + return "block.ZswapStatusController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *ZswapStatusController) Inputs() []controller.Input { + return []controller.Input{ + { + // not really a dependency, but we refresh zswap status kernel param change + Namespace: runtime.NamespaceName, + Type: runtime.KernelParamStatusType, + Kind: controller.InputWeak, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *ZswapStatusController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: block.ZswapStatusType, + Kind: controller.OutputExclusive, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *ZswapStatusController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + // in container mode, no zswap applies + if ctrl.V1Alpha1Mode == machineruntime.ModeContainer { + return nil + } + + // there is no way to watch for zswap status, so we are going to poll every minute + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + case <-ticker.C: + } + + r.StartTrackingOutputs() + + // try to read a single status file to see if zswap is enabled + if _, err := os.ReadFile("/sys/kernel/debug/zswap/pool_total_size"); err == nil { + if err = safe.WriterModify(ctx, r, block.NewZswapStatus(block.NamespaceName, block.ZswapStatusID), + func(zs *block.ZswapStatus) error { + for _, p := range []struct { + name string + out *uint64 + }{ + {"pool_total_size", &zs.TypedSpec().TotalSizeBytes}, + {"stored_pages", &zs.TypedSpec().StoredPages}, + {"pool_limit_hit", &zs.TypedSpec().PoolLimitHit}, + {"reject_reclaim_fail", &zs.TypedSpec().RejectReclaimFail}, + {"reject_alloc_fail", &zs.TypedSpec().RejectAllocFail}, + {"reject_kmemcache_fail", &zs.TypedSpec().RejectKmemcacheFail}, + {"reject_compress_fail", &zs.TypedSpec().RejectCompressFail}, + {"reject_compress_poor", &zs.TypedSpec().RejectCompressPoor}, + {"written_back_pages", &zs.TypedSpec().WrittenBackPages}, + } { + if err := ctrl.readZswapParam(p.name, p.out); err != nil { + return err + } + } + + zs.TypedSpec().TotalSizeHuman = humanize.IBytes(zs.TypedSpec().TotalSizeBytes) + + return nil + }, + ); err != nil { + return fmt.Errorf("failed to create zswap status: %w", err) + } + } + + if err := safe.CleanupOutputs[*block.ZswapStatus](ctx, r); err != nil { + return fmt.Errorf("failed to cleanup outputs: %w", err) + } + } +} + +func (ctrl *ZswapStatusController) readZswapParam(name string, out *uint64) error { + content, err := os.ReadFile(filepath.Join("/sys/kernel/debug/zswap", name)) + if err != nil { + return fmt.Errorf("failed to read zswap parameter %q: %w", name, err) + } + + *out, err = strconv.ParseUint(string(bytes.TrimSpace(content)), 10, 64) + if err != nil { + return fmt.Errorf("failed to parse zswap parameter %q: %w", name, err) + } + + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go index 9a600648d21..5fccf512305 100644 --- a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go +++ b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go @@ -110,6 +110,10 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(), }, &block.VolumeManagerController{}, + &block.ZswapConfigController{}, + &block.ZswapStatusController{ + V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(), + }, &cluster.AffiliateMergeController{}, cluster.NewConfigController(), &cluster.DiscoveryServiceController{}, diff --git a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go index 25e97752e36..ae1bdfd003c 100644 --- a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go +++ b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go @@ -112,6 +112,7 @@ func NewState() (*State, error) { &block.VolumeMountRequest{}, &block.VolumeMountStatus{}, &block.VolumeStatus{}, + &block.ZswapStatus{}, &cluster.Affiliate{}, &cluster.Config{}, &cluster.Identity{}, diff --git a/internal/app/machined/pkg/startup/cgroups.go b/internal/app/machined/pkg/startup/cgroups.go index cce62e5ea16..57d067bf4de 100644 --- a/internal/app/machined/pkg/startup/cgroups.go +++ b/internal/app/machined/pkg/startup/cgroups.go @@ -152,9 +152,10 @@ func CreateSystemCgroups(ctx context.Context, log *zap.Logger, rt runtime.Runtim name: constants.CgroupApid, resources: &cgroup2.Resources{ Memory: &cgroup2.Memory{ - Min: pointer.To[int64](constants.CgroupApidReservedMemory), - Low: pointer.To[int64](constants.CgroupApidReservedMemory * 2), - Max: zeroIfRace(pointer.To[int64](constants.CgroupApidMaxMemory)), + Min: pointer.To[int64](constants.CgroupApidReservedMemory), + Low: pointer.To[int64](constants.CgroupApidReservedMemory * 2), + Max: zeroIfRace(pointer.To[int64](constants.CgroupApidMaxMemory)), + Swap: pointer.To[int64](0), }, CPU: &cgroup2.CPU{ Weight: pointer.To[uint64](cgroup.MillicoresToCPUWeight(cgroup.MilliCores(constants.CgroupApidMillicores))), @@ -165,9 +166,10 @@ func CreateSystemCgroups(ctx context.Context, log *zap.Logger, rt runtime.Runtim name: constants.CgroupTrustd, resources: &cgroup2.Resources{ Memory: &cgroup2.Memory{ - Min: pointer.To[int64](constants.CgroupTrustdReservedMemory), - Low: pointer.To[int64](constants.CgroupTrustdReservedMemory * 2), - Max: zeroIfRace(pointer.To[int64](constants.CgroupTrustdMaxMemory)), + Min: pointer.To[int64](constants.CgroupTrustdReservedMemory), + Low: pointer.To[int64](constants.CgroupTrustdReservedMemory * 2), + Max: zeroIfRace(pointer.To[int64](constants.CgroupTrustdMaxMemory)), + Swap: pointer.To[int64](0), }, CPU: &cgroup2.CPU{ Weight: pointer.To[uint64](cgroup.MillicoresToCPUWeight(cgroup.MilliCores(constants.CgroupTrustdMillicores))), diff --git a/internal/integration/api/volumes.go b/internal/integration/api/volumes.go index 3f42d73a64a..cf35a15dbfb 100644 --- a/internal/integration/api/volumes.go +++ b/internal/integration/api/volumes.go @@ -755,6 +755,29 @@ func (suite *VolumesSuite) TestSwapOnOff() { })) } +// TestZswapStatus verifies that all zswap-enabled machines have zswap running. +func (suite *VolumesSuite) TestZswapStatus() { + for _, node := range suite.DiscoverNodeInternalIPs(suite.ctx) { + suite.Run(node, func() { + ctx := client.WithNode(suite.ctx, node) + + cfg, err := suite.ReadConfigFromNode(ctx) + suite.Require().NoError(err) + + if cfg.ZswapConfig() == nil { + suite.T().Skipf("skipping test, zswap is not enabled on node %s", node) + } + + rtestutils.AssertResource(ctx, suite.T(), suite.Client.COSI, + block.ZswapStatusID, + func(vs *block.ZswapStatus, asrt *assert.Assertions) { + suite.T().Logf("zswap total size %s, stored pages %d", vs.TypedSpec().TotalSizeHuman, vs.TypedSpec().StoredPages) + }, + ) + }) + } +} + func init() { allSuites = append(allSuites, new(VolumesSuite)) } diff --git a/internal/pkg/cgroups/cgroups.go b/internal/pkg/cgroups/cgroups.go index dee24ad235b..47d86f8c42f 100644 --- a/internal/pkg/cgroups/cgroups.go +++ b/internal/pkg/cgroups/cgroups.go @@ -134,6 +134,10 @@ type Node struct { MemorySwapMax Value MemorySwapPeak Value + MemoryZswapCurrent Value + MemoryZswapMax Value + MemoryZswapWriteback Value + PidsCurrent Value PidsEvents FlatMap PidsMax Value @@ -280,6 +284,12 @@ func (n *Node) Parse(filename string, r io.Reader) error { return parseSingleValue(ParseNewlineSeparatedValues, &n.MemorySwapMax, r) case "memory.swap.peak": return parseSingleValue(ParseNewlineSeparatedValues, &n.MemorySwapPeak, r) + case "memory.zswap.current": + return parseSingleValue(ParseNewlineSeparatedValues, &n.MemoryZswapCurrent, r) + case "memory.zswap.max": + return parseSingleValue(ParseNewlineSeparatedValues, &n.MemoryZswapMax, r) + case "memory.zswap.writeback": + return parseSingleValue(ParseNewlineSeparatedValues, &n.MemoryZswapWriteback, r) case "pids.current": return parseSingleValue(ParseNewlineSeparatedValues, &n.PidsCurrent, r) case "pids.events": diff --git a/internal/pkg/mount/v2/pseudo.go b/internal/pkg/mount/v2/pseudo.go index 0ecd6ced992..863ccf22f52 100644 --- a/internal/pkg/mount/v2/pseudo.go +++ b/internal/pkg/mount/v2/pseudo.go @@ -41,6 +41,7 @@ func PseudoSubMountPoints() Points { NewPoint("securityfs", "/sys/kernel/security", "securityfs", WithFlags(unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV|unix.MS_RELATIME)), NewPoint("tracefs", "/sys/kernel/tracing", "tracefs", WithFlags(unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV)), NewPoint("configfs", "/sys/kernel/config", "configfs", WithFlags(unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV|unix.MS_RELATIME)), + NewPoint("debugfs", "/sys/kernel/debug", "debugfs", WithFlags(unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV|unix.MS_RELATIME)), } if _, err := os.Stat(constants.EFIVarsMountPoint); err == nil { diff --git a/pkg/machinery/api/resource/definitions/block/block.pb.go b/pkg/machinery/api/resource/definitions/block/block.pb.go index 4d1b02ac29d..5553c0fb6d5 100644 --- a/pkg/machinery/api/resource/definitions/block/block.pb.go +++ b/pkg/machinery/api/resource/definitions/block/block.pb.go @@ -2025,6 +2025,123 @@ func (x *VolumeStatusSpec) GetParentId() string { return "" } +// ZswapStatusSpec is the spec for ZswapStatus resource. +type ZswapStatusSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + TotalSizeBytes uint64 `protobuf:"varint,1,opt,name=total_size_bytes,json=totalSizeBytes,proto3" json:"total_size_bytes,omitempty"` + TotalSizeHuman string `protobuf:"bytes,2,opt,name=total_size_human,json=totalSizeHuman,proto3" json:"total_size_human,omitempty"` + StoredPages uint64 `protobuf:"varint,3,opt,name=stored_pages,json=storedPages,proto3" json:"stored_pages,omitempty"` + PoolLimitHit uint64 `protobuf:"varint,4,opt,name=pool_limit_hit,json=poolLimitHit,proto3" json:"pool_limit_hit,omitempty"` + RejectReclaimFail uint64 `protobuf:"varint,5,opt,name=reject_reclaim_fail,json=rejectReclaimFail,proto3" json:"reject_reclaim_fail,omitempty"` + RejectAllocFail uint64 `protobuf:"varint,6,opt,name=reject_alloc_fail,json=rejectAllocFail,proto3" json:"reject_alloc_fail,omitempty"` + RejectKmemcacheFail uint64 `protobuf:"varint,7,opt,name=reject_kmemcache_fail,json=rejectKmemcacheFail,proto3" json:"reject_kmemcache_fail,omitempty"` + RejectCompressFail uint64 `protobuf:"varint,8,opt,name=reject_compress_fail,json=rejectCompressFail,proto3" json:"reject_compress_fail,omitempty"` + RejectCompressPoor uint64 `protobuf:"varint,9,opt,name=reject_compress_poor,json=rejectCompressPoor,proto3" json:"reject_compress_poor,omitempty"` + WrittenBackPages uint64 `protobuf:"varint,10,opt,name=written_back_pages,json=writtenBackPages,proto3" json:"written_back_pages,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ZswapStatusSpec) Reset() { + *x = ZswapStatusSpec{} + mi := &file_resource_definitions_block_block_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ZswapStatusSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ZswapStatusSpec) ProtoMessage() {} + +func (x *ZswapStatusSpec) ProtoReflect() protoreflect.Message { + mi := &file_resource_definitions_block_block_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ZswapStatusSpec.ProtoReflect.Descriptor instead. +func (*ZswapStatusSpec) Descriptor() ([]byte, []int) { + return file_resource_definitions_block_block_proto_rawDescGZIP(), []int{24} +} + +func (x *ZswapStatusSpec) GetTotalSizeBytes() uint64 { + if x != nil { + return x.TotalSizeBytes + } + return 0 +} + +func (x *ZswapStatusSpec) GetTotalSizeHuman() string { + if x != nil { + return x.TotalSizeHuman + } + return "" +} + +func (x *ZswapStatusSpec) GetStoredPages() uint64 { + if x != nil { + return x.StoredPages + } + return 0 +} + +func (x *ZswapStatusSpec) GetPoolLimitHit() uint64 { + if x != nil { + return x.PoolLimitHit + } + return 0 +} + +func (x *ZswapStatusSpec) GetRejectReclaimFail() uint64 { + if x != nil { + return x.RejectReclaimFail + } + return 0 +} + +func (x *ZswapStatusSpec) GetRejectAllocFail() uint64 { + if x != nil { + return x.RejectAllocFail + } + return 0 +} + +func (x *ZswapStatusSpec) GetRejectKmemcacheFail() uint64 { + if x != nil { + return x.RejectKmemcacheFail + } + return 0 +} + +func (x *ZswapStatusSpec) GetRejectCompressFail() uint64 { + if x != nil { + return x.RejectCompressFail + } + return 0 +} + +func (x *ZswapStatusSpec) GetRejectCompressPoor() uint64 { + if x != nil { + return x.RejectCompressPoor + } + return 0 +} + +func (x *ZswapStatusSpec) GetWrittenBackPages() uint64 { + if x != nil { + return x.WrittenBackPages + } + return 0 +} + var File_resource_definitions_block_block_proto protoreflect.FileDescriptor const file_resource_definitions_block_block_proto_rawDesc = "" + @@ -2225,7 +2342,19 @@ const file_resource_definitions_block_block_proto_rawDesc = "" + "\x04type\x18\x10 \x01(\x0e21.talos.resource.definitions.enums.BlockVolumeTypeR\x04type\x12<\n" + "\x1aconfigured_encryption_keys\x18\x11 \x03(\tR\x18configuredEncryptionKeys\x12\\\n" + "\fsymlink_spec\x18\x12 \x01(\v29.talos.resource.definitions.block.SymlinkProvisioningSpecR\vsymlinkSpec\x12\x1b\n" + - "\tparent_id\x18\x13 \x01(\tR\bparentIdBt\n" + + "\tparent_id\x18\x13 \x01(\tR\bparentId\"\xd0\x03\n" + + "\x0fZswapStatusSpec\x12(\n" + + "\x10total_size_bytes\x18\x01 \x01(\x04R\x0etotalSizeBytes\x12(\n" + + "\x10total_size_human\x18\x02 \x01(\tR\x0etotalSizeHuman\x12!\n" + + "\fstored_pages\x18\x03 \x01(\x04R\vstoredPages\x12$\n" + + "\x0epool_limit_hit\x18\x04 \x01(\x04R\fpoolLimitHit\x12.\n" + + "\x13reject_reclaim_fail\x18\x05 \x01(\x04R\x11rejectReclaimFail\x12*\n" + + "\x11reject_alloc_fail\x18\x06 \x01(\x04R\x0frejectAllocFail\x122\n" + + "\x15reject_kmemcache_fail\x18\a \x01(\x04R\x13rejectKmemcacheFail\x120\n" + + "\x14reject_compress_fail\x18\b \x01(\x04R\x12rejectCompressFail\x120\n" + + "\x14reject_compress_poor\x18\t \x01(\x04R\x12rejectCompressPoor\x12,\n" + + "\x12written_back_pages\x18\n" + + " \x01(\x04R\x10writtenBackPagesBt\n" + "(dev.talos.api.resource.definitions.blockZHgithub.com/siderolabs/talos/pkg/machinery/api/resource/definitions/blockb\x06proto3" var ( @@ -2240,7 +2369,7 @@ func file_resource_definitions_block_block_proto_rawDescGZIP() []byte { return file_resource_definitions_block_block_proto_rawDescData } -var file_resource_definitions_block_block_proto_msgTypes = make([]protoimpl.MessageInfo, 24) +var file_resource_definitions_block_block_proto_msgTypes = make([]protoimpl.MessageInfo, 25) var file_resource_definitions_block_block_proto_goTypes = []any{ (*DeviceSpec)(nil), // 0: talos.resource.definitions.block.DeviceSpec (*DiscoveredVolumeSpec)(nil), // 1: talos.resource.definitions.block.DiscoveredVolumeSpec @@ -2266,38 +2395,39 @@ var file_resource_definitions_block_block_proto_goTypes = []any{ (*VolumeMountRequestSpec)(nil), // 21: talos.resource.definitions.block.VolumeMountRequestSpec (*VolumeMountStatusSpec)(nil), // 22: talos.resource.definitions.block.VolumeMountStatusSpec (*VolumeStatusSpec)(nil), // 23: talos.resource.definitions.block.VolumeStatusSpec - (*v1alpha1.CheckedExpr)(nil), // 24: google.api.expr.v1alpha1.CheckedExpr - (enums.BlockEncryptionKeyType)(0), // 25: talos.resource.definitions.enums.BlockEncryptionKeyType - (enums.BlockEncryptionProviderType)(0), // 26: talos.resource.definitions.enums.BlockEncryptionProviderType - (enums.BlockFilesystemType)(0), // 27: talos.resource.definitions.enums.BlockFilesystemType - (enums.BlockVolumeType)(0), // 28: talos.resource.definitions.enums.BlockVolumeType - (enums.BlockVolumePhase)(0), // 29: talos.resource.definitions.enums.BlockVolumePhase + (*ZswapStatusSpec)(nil), // 24: talos.resource.definitions.block.ZswapStatusSpec + (*v1alpha1.CheckedExpr)(nil), // 25: google.api.expr.v1alpha1.CheckedExpr + (enums.BlockEncryptionKeyType)(0), // 26: talos.resource.definitions.enums.BlockEncryptionKeyType + (enums.BlockEncryptionProviderType)(0), // 27: talos.resource.definitions.enums.BlockEncryptionProviderType + (enums.BlockFilesystemType)(0), // 28: talos.resource.definitions.enums.BlockFilesystemType + (enums.BlockVolumeType)(0), // 29: talos.resource.definitions.enums.BlockVolumeType + (enums.BlockVolumePhase)(0), // 30: talos.resource.definitions.enums.BlockVolumePhase } var file_resource_definitions_block_block_proto_depIdxs = []int32{ - 24, // 0: talos.resource.definitions.block.DiskSelector.match:type_name -> google.api.expr.v1alpha1.CheckedExpr - 25, // 1: talos.resource.definitions.block.EncryptionKey.type:type_name -> talos.resource.definitions.enums.BlockEncryptionKeyType - 26, // 2: talos.resource.definitions.block.EncryptionSpec.provider:type_name -> talos.resource.definitions.enums.BlockEncryptionProviderType + 25, // 0: talos.resource.definitions.block.DiskSelector.match:type_name -> google.api.expr.v1alpha1.CheckedExpr + 26, // 1: talos.resource.definitions.block.EncryptionKey.type:type_name -> talos.resource.definitions.enums.BlockEncryptionKeyType + 27, // 2: talos.resource.definitions.block.EncryptionSpec.provider:type_name -> talos.resource.definitions.enums.BlockEncryptionProviderType 6, // 3: talos.resource.definitions.block.EncryptionSpec.keys:type_name -> talos.resource.definitions.block.EncryptionKey - 27, // 4: talos.resource.definitions.block.FilesystemSpec.type:type_name -> talos.resource.definitions.enums.BlockFilesystemType - 24, // 5: talos.resource.definitions.block.LocatorSpec.match:type_name -> google.api.expr.v1alpha1.CheckedExpr + 28, // 4: talos.resource.definitions.block.FilesystemSpec.type:type_name -> talos.resource.definitions.enums.BlockFilesystemType + 25, // 5: talos.resource.definitions.block.LocatorSpec.match:type_name -> google.api.expr.v1alpha1.CheckedExpr 10, // 6: talos.resource.definitions.block.MountStatusSpec.spec:type_name -> talos.resource.definitions.block.MountRequestSpec - 27, // 7: talos.resource.definitions.block.MountStatusSpec.filesystem:type_name -> talos.resource.definitions.enums.BlockFilesystemType - 26, // 8: talos.resource.definitions.block.MountStatusSpec.encryption_provider:type_name -> talos.resource.definitions.enums.BlockEncryptionProviderType + 28, // 7: talos.resource.definitions.block.MountStatusSpec.filesystem:type_name -> talos.resource.definitions.enums.BlockFilesystemType + 27, // 8: talos.resource.definitions.block.MountStatusSpec.encryption_provider:type_name -> talos.resource.definitions.enums.BlockEncryptionProviderType 4, // 9: talos.resource.definitions.block.ProvisioningSpec.disk_selector:type_name -> talos.resource.definitions.block.DiskSelector 13, // 10: talos.resource.definitions.block.ProvisioningSpec.partition_spec:type_name -> talos.resource.definitions.block.PartitionSpec 8, // 11: talos.resource.definitions.block.ProvisioningSpec.filesystem_spec:type_name -> talos.resource.definitions.block.FilesystemSpec - 28, // 12: talos.resource.definitions.block.VolumeConfigSpec.type:type_name -> talos.resource.definitions.enums.BlockVolumeType + 29, // 12: talos.resource.definitions.block.VolumeConfigSpec.type:type_name -> talos.resource.definitions.enums.BlockVolumeType 14, // 13: talos.resource.definitions.block.VolumeConfigSpec.provisioning:type_name -> talos.resource.definitions.block.ProvisioningSpec 9, // 14: talos.resource.definitions.block.VolumeConfigSpec.locator:type_name -> talos.resource.definitions.block.LocatorSpec 11, // 15: talos.resource.definitions.block.VolumeConfigSpec.mount:type_name -> talos.resource.definitions.block.MountSpec 7, // 16: talos.resource.definitions.block.VolumeConfigSpec.encryption:type_name -> talos.resource.definitions.block.EncryptionSpec 16, // 17: talos.resource.definitions.block.VolumeConfigSpec.symlink:type_name -> talos.resource.definitions.block.SymlinkProvisioningSpec - 29, // 18: talos.resource.definitions.block.VolumeStatusSpec.phase:type_name -> talos.resource.definitions.enums.BlockVolumePhase - 29, // 19: talos.resource.definitions.block.VolumeStatusSpec.pre_fail_phase:type_name -> talos.resource.definitions.enums.BlockVolumePhase - 27, // 20: talos.resource.definitions.block.VolumeStatusSpec.filesystem:type_name -> talos.resource.definitions.enums.BlockFilesystemType - 26, // 21: talos.resource.definitions.block.VolumeStatusSpec.encryption_provider:type_name -> talos.resource.definitions.enums.BlockEncryptionProviderType + 30, // 18: talos.resource.definitions.block.VolumeStatusSpec.phase:type_name -> talos.resource.definitions.enums.BlockVolumePhase + 30, // 19: talos.resource.definitions.block.VolumeStatusSpec.pre_fail_phase:type_name -> talos.resource.definitions.enums.BlockVolumePhase + 28, // 20: talos.resource.definitions.block.VolumeStatusSpec.filesystem:type_name -> talos.resource.definitions.enums.BlockFilesystemType + 27, // 21: talos.resource.definitions.block.VolumeStatusSpec.encryption_provider:type_name -> talos.resource.definitions.enums.BlockEncryptionProviderType 11, // 22: talos.resource.definitions.block.VolumeStatusSpec.mount_spec:type_name -> talos.resource.definitions.block.MountSpec - 28, // 23: talos.resource.definitions.block.VolumeStatusSpec.type:type_name -> talos.resource.definitions.enums.BlockVolumeType + 29, // 23: talos.resource.definitions.block.VolumeStatusSpec.type:type_name -> talos.resource.definitions.enums.BlockVolumeType 16, // 24: talos.resource.definitions.block.VolumeStatusSpec.symlink_spec:type_name -> talos.resource.definitions.block.SymlinkProvisioningSpec 25, // [25:25] is the sub-list for method output_type 25, // [25:25] is the sub-list for method input_type @@ -2317,7 +2447,7 @@ func file_resource_definitions_block_block_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_resource_definitions_block_block_proto_rawDesc), len(file_resource_definitions_block_block_proto_rawDesc)), NumEnums: 0, - NumMessages: 24, + NumMessages: 25, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/machinery/api/resource/definitions/block/block_vtproto.pb.go b/pkg/machinery/api/resource/definitions/block/block_vtproto.pb.go index ad2c385869c..396d32715e6 100644 --- a/pkg/machinery/api/resource/definitions/block/block_vtproto.pb.go +++ b/pkg/machinery/api/resource/definitions/block/block_vtproto.pb.go @@ -1859,6 +1859,91 @@ func (m *VolumeStatusSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *ZswapStatusSpec) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ZswapStatusSpec) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *ZswapStatusSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.WrittenBackPages != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.WrittenBackPages)) + i-- + dAtA[i] = 0x50 + } + if m.RejectCompressPoor != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.RejectCompressPoor)) + i-- + dAtA[i] = 0x48 + } + if m.RejectCompressFail != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.RejectCompressFail)) + i-- + dAtA[i] = 0x40 + } + if m.RejectKmemcacheFail != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.RejectKmemcacheFail)) + i-- + dAtA[i] = 0x38 + } + if m.RejectAllocFail != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.RejectAllocFail)) + i-- + dAtA[i] = 0x30 + } + if m.RejectReclaimFail != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.RejectReclaimFail)) + i-- + dAtA[i] = 0x28 + } + if m.PoolLimitHit != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.PoolLimitHit)) + i-- + dAtA[i] = 0x20 + } + if m.StoredPages != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.StoredPages)) + i-- + dAtA[i] = 0x18 + } + if len(m.TotalSizeHuman) > 0 { + i -= len(m.TotalSizeHuman) + copy(dAtA[i:], m.TotalSizeHuman) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.TotalSizeHuman))) + i-- + dAtA[i] = 0x12 + } + if m.TotalSizeBytes != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.TotalSizeBytes)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + func (m *DeviceSpec) SizeVT() (n int) { if m == nil { return 0 @@ -2631,6 +2716,47 @@ func (m *VolumeStatusSpec) SizeVT() (n int) { return n } +func (m *ZswapStatusSpec) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.TotalSizeBytes != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.TotalSizeBytes)) + } + l = len(m.TotalSizeHuman) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.StoredPages != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.StoredPages)) + } + if m.PoolLimitHit != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.PoolLimitHit)) + } + if m.RejectReclaimFail != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.RejectReclaimFail)) + } + if m.RejectAllocFail != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.RejectAllocFail)) + } + if m.RejectKmemcacheFail != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.RejectKmemcacheFail)) + } + if m.RejectCompressFail != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.RejectCompressFail)) + } + if m.RejectCompressPoor != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.RejectCompressPoor)) + } + if m.WrittenBackPages != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.WrittenBackPages)) + } + n += len(m.unknownFields) + return n +} + func (m *DeviceSpec) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -7662,3 +7788,257 @@ func (m *VolumeStatusSpec) UnmarshalVT(dAtA []byte) error { } return nil } +func (m *ZswapStatusSpec) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ZswapStatusSpec: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ZswapStatusSpec: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field TotalSizeBytes", wireType) + } + m.TotalSizeBytes = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.TotalSizeBytes |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field TotalSizeHuman", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.TotalSizeHuman = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field StoredPages", wireType) + } + m.StoredPages = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.StoredPages |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field PoolLimitHit", wireType) + } + m.PoolLimitHit = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.PoolLimitHit |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field RejectReclaimFail", wireType) + } + m.RejectReclaimFail = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.RejectReclaimFail |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 6: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field RejectAllocFail", wireType) + } + m.RejectAllocFail = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.RejectAllocFail |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 7: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field RejectKmemcacheFail", wireType) + } + m.RejectKmemcacheFail = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.RejectKmemcacheFail |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 8: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field RejectCompressFail", wireType) + } + m.RejectCompressFail = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.RejectCompressFail |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 9: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field RejectCompressPoor", wireType) + } + m.RejectCompressPoor = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.RejectCompressPoor |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 10: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field WrittenBackPages", wireType) + } + m.WrittenBackPages = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.WrittenBackPages |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} diff --git a/pkg/machinery/config/config/config.go b/pkg/machinery/config/config/config.go index 7303cc54369..f616668a193 100644 --- a/pkg/machinery/config/config/config.go +++ b/pkg/machinery/config/config/config.go @@ -21,4 +21,5 @@ type Config interface { //nolint:interfacebloat EthernetConfigs() []EthernetConfig UserVolumeConfigs() []UserVolumeConfig SwapVolumeConfigs() []SwapVolumeConfig + ZswapConfig() ZswapConfig } diff --git a/pkg/machinery/config/config/volume.go b/pkg/machinery/config/config/volume.go index 6ae6dab8dff..e222bc06eb9 100644 --- a/pkg/machinery/config/config/volume.go +++ b/pkg/machinery/config/config/volume.go @@ -98,3 +98,10 @@ type SwapVolumeConfig interface { Provisioning() VolumeProvisioningConfig Encryption() EncryptionConfig } + +// ZswapConfig defines the interface to access zswap configuration. +type ZswapConfig interface { + ZswapConfigSignal() + MaxPoolPercent() int + ShrinkerEnabled() bool +} diff --git a/pkg/machinery/config/container/container.go b/pkg/machinery/config/container/container.go index 802d2fd6eda..ab80cbe80bf 100644 --- a/pkg/machinery/config/container/container.go +++ b/pkg/machinery/config/container/container.go @@ -230,6 +230,16 @@ func (container *Container) SwapVolumeConfigs() []config.SwapVolumeConfig { return findMatchingDocs[config.SwapVolumeConfig](container.documents) } +// ZswapConfig implements config.Config interface. +func (container *Container) ZswapConfig() config.ZswapConfig { + matching := findMatchingDocs[config.ZswapConfig](container.documents) + if len(matching) == 0 { + return nil + } + + return matching[0] +} + // Bytes returns source YAML representation (if available) or does default encoding. func (container *Container) Bytes() ([]byte, error) { if !container.readonly { diff --git a/pkg/machinery/config/schemas/config.schema.json b/pkg/machinery/config/schemas/config.schema.json index aa1de056032..3d259d8d792 100644 --- a/pkg/machinery/config/schemas/config.schema.json +++ b/pkg/machinery/config/schemas/config.schema.json @@ -370,6 +370,49 @@ ], "description": "VolumeConfig is a system volume configuration document.\\nNote: at the moment, only `EPHEMERAL` and `IMAGE-CACHE` system volumes are supported.\\n" }, + "block.ZswapConfigV1Alpha1": { + "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": [ + "ZswapConfig" + ], + "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" + }, + "maxPoolPercent": { + "type": "integer", + "title": "maxPoolPercent", + "description": "The maximum percent of memory that zswap can use.\nThis is a percentage of the total system memory.\nThe value must be between 0 and 100.\n", + "markdownDescription": "The maximum percent of memory that zswap can use.\nThis is a percentage of the total system memory.\nThe value must be between 0 and 100.", + "x-intellij-html-description": "\u003cp\u003eThe maximum percent of memory that zswap can use.\nThis is a percentage of the total system memory.\nThe value must be between 0 and 100.\u003c/p\u003e\n" + }, + "shrinkerEnabled": { + "type": "boolean", + "title": "shrinkerEnabled", + "description": "Enable the shrinker feature: kernel might move\ncold pages from zswap to swap device to free up memory\nfor other use cases.\n", + "markdownDescription": "Enable the shrinker feature: kernel might move\ncold pages from zswap to swap device to free up memory\nfor other use cases.", + "x-intellij-html-description": "\u003cp\u003eEnable the shrinker feature: kernel might move\ncold pages from zswap to swap device to free up memory\nfor other use cases.\u003c/p\u003e\n" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "apiVersion", + "kind" + ], + "description": "ZswapConfig is a zswap (compressed memory) configuration document.\\nWhen zswap is enabled, Linux kernel compresses pages that would otherwise be swapped out to disk.\\nThe compressed pages are stored in a memory pool, which is used to avoid writing to disk\\nwhen the system is under memory pressure.\\n" + }, "extensions.ConfigFile": { "properties": { "content": { @@ -4242,6 +4285,9 @@ { "$ref": "#/$defs/block.VolumeConfigV1Alpha1" }, + { + "$ref": "#/$defs/block.ZswapConfigV1Alpha1" + }, { "$ref": "#/$defs/extensions.ServiceConfigV1Alpha1" }, diff --git a/pkg/machinery/config/types/block/block.go b/pkg/machinery/config/types/block/block.go index f054ae4b06c..ce95750847d 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 docgen -output block_doc.go block.go encryption.go swap_volume_config.go user_volume_config.go volume_config.go +//go:generate docgen -output block_doc.go block.go encryption.go swap_volume_config.go user_volume_config.go volume_config.go zswap_config.go -//go:generate deep-copy -type SwapVolumeConfigV1Alpha1 -type UserVolumeConfigV1Alpha1 -type VolumeConfigV1Alpha1 -pointer-receiver -header-file ../../../../../hack/boilerplate.txt -o deep_copy.generated.go . +//go:generate deep-copy -type SwapVolumeConfigV1Alpha1 -type UserVolumeConfigV1Alpha1 -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 417ed257790..e1055d2326a 100644 --- a/pkg/machinery/config/types/block/block_doc.go +++ b/pkg/machinery/config/types/block/block_doc.go @@ -462,6 +462,35 @@ func (DiskSelector) Doc() *encoder.Doc { return doc } +func (ZswapConfigV1Alpha1) Doc() *encoder.Doc { + doc := &encoder.Doc{ + Type: "ZswapConfig", + Comments: [3]string{"" /* encoder.HeadComment */, "ZswapConfig is a zswap (compressed memory) configuration document." /* encoder.LineComment */, "" /* encoder.FootComment */}, + Description: "ZswapConfig is a zswap (compressed memory) configuration document.\nWhen zswap is enabled, Linux kernel compresses pages that would otherwise be swapped out to disk.\nThe compressed pages are stored in a memory pool, which is used to avoid writing to disk\nwhen the system is under memory pressure.\n", + Fields: []encoder.Doc{ + {}, + { + Name: "maxPoolPercent", + Type: "int", + Note: "", + Description: "The maximum percent of memory that zswap can use.\nThis is a percentage of the total system memory.\nThe value must be between 0 and 100.", + Comments: [3]string{"" /* encoder.HeadComment */, "The maximum percent of memory that zswap can use." /* encoder.LineComment */, "" /* encoder.FootComment */}, + }, + { + Name: "shrinkerEnabled", + Type: "bool", + Note: "", + Description: "Enable the shrinker feature: kernel might move\ncold pages from zswap to swap device to free up memory\nfor other use cases.", + Comments: [3]string{"" /* encoder.HeadComment */, "Enable the shrinker feature: kernel might move" /* encoder.LineComment */, "" /* encoder.FootComment */}, + }, + }, + } + + doc.AddExample("", exampleZswapConfigV1Alpha1()) + + return doc +} + // GetFileDoc returns documentation for the file block_doc.go. func GetFileDoc() *encoder.FileDoc { return &encoder.FileDoc{ @@ -480,6 +509,7 @@ func GetFileDoc() *encoder.FileDoc { VolumeConfigV1Alpha1{}.Doc(), ProvisioningSpec{}.Doc(), DiskSelector{}.Doc(), + ZswapConfigV1Alpha1{}.Doc(), }, } } diff --git a/pkg/machinery/config/types/block/deep_copy.generated.go b/pkg/machinery/config/types/block/deep_copy.generated.go index 4710a21f0b8..47ad4e53e8c 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 SwapVolumeConfigV1Alpha1 -type UserVolumeConfigV1Alpha1 -type VolumeConfigV1Alpha1 -pointer-receiver -header-file ../../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT. +// Code generated by "deep-copy -type SwapVolumeConfigV1Alpha1 -type UserVolumeConfigV1Alpha1 -type VolumeConfigV1Alpha1 -type ZswapConfigV1Alpha1 -pointer-receiver -header-file ../../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT. package block @@ -143,3 +143,17 @@ func (o *VolumeConfigV1Alpha1) DeepCopy() *VolumeConfigV1Alpha1 { } return &cp } + +// DeepCopy generates a deep copy of *ZswapConfigV1Alpha1. +func (o *ZswapConfigV1Alpha1) DeepCopy() *ZswapConfigV1Alpha1 { + var cp ZswapConfigV1Alpha1 = *o + if o.MaxPoolPercentConfig != nil { + cp.MaxPoolPercentConfig = new(int) + *cp.MaxPoolPercentConfig = *o.MaxPoolPercentConfig + } + if o.ShrinkerEnabledConfig != nil { + cp.ShrinkerEnabledConfig = new(bool) + *cp.ShrinkerEnabledConfig = *o.ShrinkerEnabledConfig + } + return &cp +} diff --git a/pkg/machinery/config/types/block/testdata/zswapconfig_full.yaml b/pkg/machinery/config/types/block/testdata/zswapconfig_full.yaml new file mode 100644 index 00000000000..156bc23ed47 --- /dev/null +++ b/pkg/machinery/config/types/block/testdata/zswapconfig_full.yaml @@ -0,0 +1,4 @@ +apiVersion: v1alpha1 +kind: ZswapConfig +maxPoolPercent: 50 +shrinkerEnabled: true diff --git a/pkg/machinery/config/types/block/testdata/zswapconfig_min.yaml b/pkg/machinery/config/types/block/testdata/zswapconfig_min.yaml new file mode 100644 index 00000000000..b3c94e47eea --- /dev/null +++ b/pkg/machinery/config/types/block/testdata/zswapconfig_min.yaml @@ -0,0 +1,2 @@ +apiVersion: v1alpha1 +kind: ZswapConfig diff --git a/pkg/machinery/config/types/block/zswap_config.go b/pkg/machinery/config/types/block/zswap_config.go new file mode 100644 index 00000000000..4766166dbe5 --- /dev/null +++ b/pkg/machinery/config/types/block/zswap_config.go @@ -0,0 +1,126 @@ +// 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" + + "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" +) + +// ZswapConfigKind is a config document kind. +const ZswapConfigKind = "ZswapConfig" + +func init() { + registry.Register(ZswapConfigKind, func(version string) config.Document { + switch version { + case "v1alpha1": //nolint:goconst + return &ZswapConfigV1Alpha1{} + default: + return nil + } + }) +} + +// Check interfaces. +var ( + _ config.ZswapConfig = &ZswapConfigV1Alpha1{} + _ config.Validator = &ZswapConfigV1Alpha1{} +) + +// ZswapConfigV1Alpha1 is a zswap (compressed memory) configuration document. +// +// description: | +// When zswap is enabled, Linux kernel compresses pages that would otherwise be swapped out to disk. +// The compressed pages are stored in a memory pool, which is used to avoid writing to disk +// when the system is under memory pressure. +// examples: +// - value: exampleZswapConfigV1Alpha1() +// alias: ZswapConfig +// schemaRoot: true +// schemaMeta: v1alpha1/ZswapConfig +type ZswapConfigV1Alpha1 struct { + meta.Meta `yaml:",inline"` + // description: | + // The maximum percent of memory that zswap can use. + // This is a percentage of the total system memory. + // The value must be between 0 and 100. + MaxPoolPercentConfig *int `yaml:"maxPoolPercent,omitempty"` + // description: | + // Enable the shrinker feature: kernel might move + // cold pages from zswap to swap device to free up memory + // for other use cases. + ShrinkerEnabledConfig *bool `yaml:"shrinkerEnabled,omitempty"` +} + +// NewZswapConfigV1Alpha1 creates a new zswap config document. +func NewZswapConfigV1Alpha1() *ZswapConfigV1Alpha1 { + return &ZswapConfigV1Alpha1{ + Meta: meta.Meta{ + MetaKind: ZswapConfigKind, + MetaAPIVersion: "v1alpha1", + }, + } +} + +func exampleZswapConfigV1Alpha1() *ZswapConfigV1Alpha1 { + cfg := NewZswapConfigV1Alpha1() + cfg.MaxPoolPercentConfig = pointer.To(25) + cfg.ShrinkerEnabledConfig = pointer.To(true) + + return cfg +} + +// Clone implements config.Document interface. +func (s *ZswapConfigV1Alpha1) Clone() config.Document { + return s.DeepCopy() +} + +// Validate implements config.Validator interface. +// +//nolint:gocyclo +func (s *ZswapConfigV1Alpha1) Validate(validation.RuntimeMode, ...validation.Option) ([]string, error) { + var ( + warnings []string + validationErrors error + ) + + if s.MaxPoolPercentConfig != nil { + if *s.MaxPoolPercentConfig < 0 || *s.MaxPoolPercentConfig > 100 { + validationErrors = errors.Join(validationErrors, fmt.Errorf("maxPoolPercent must be between 0 and 100")) + } + } + + return warnings, validationErrors +} + +// ZswapConfigSignal is a signal for zswap config. +func (s *ZswapConfigV1Alpha1) ZswapConfigSignal() {} + +// MaxPoolPercent implements config.ZswapConfig interface. +func (s *ZswapConfigV1Alpha1) MaxPoolPercent() int { + if s.MaxPoolPercentConfig == nil { + return 20 + } + + return pointer.SafeDeref(s.MaxPoolPercentConfig) +} + +// ShrinkerEnabled implements config.ZswapConfig interface. +func (s *ZswapConfigV1Alpha1) ShrinkerEnabled() bool { + if s.ShrinkerEnabledConfig == nil { + return false + } + + return pointer.SafeDeref(s.ShrinkerEnabledConfig) +} diff --git a/pkg/machinery/config/types/block/zswap_config_test.go b/pkg/machinery/config/types/block/zswap_config_test.go new file mode 100644 index 00000000000..93cfde20d33 --- /dev/null +++ b/pkg/machinery/config/types/block/zswap_config_test.go @@ -0,0 +1,141 @@ +// 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/siderolabs/go-pointer" + "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" +) + +func TestZswapConfigMarshalUnmarshal(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + + filename string + cfg func(t *testing.T) *block.ZswapConfigV1Alpha1 + }{ + { + name: "full config", + filename: "zswapconfig_full.yaml", + cfg: func(t *testing.T) *block.ZswapConfigV1Alpha1 { + c := block.NewZswapConfigV1Alpha1() + c.MaxPoolPercentConfig = pointer.To(50) + c.ShrinkerEnabledConfig = pointer.To(true) + + return c + }, + }, + { + name: "min config", + filename: "zswapconfig_min.yaml", + cfg: func(t *testing.T) *block.ZswapConfigV1Alpha1 { + c := block.NewZswapConfigV1Alpha1() + + return c + }, + }, + } { + 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 TestZswapVolumeConfigValidate(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + + cfg func(t *testing.T) *block.ZswapConfigV1Alpha1 + + expectedErrors string + }{ + { + name: "minimal", + + cfg: func(t *testing.T) *block.ZswapConfigV1Alpha1 { + c := block.NewZswapConfigV1Alpha1() + + return c + }, + }, + { + name: "full", + + cfg: func(t *testing.T) *block.ZswapConfigV1Alpha1 { + c := block.NewZswapConfigV1Alpha1() + c.MaxPoolPercentConfig = pointer.To(50) + c.ShrinkerEnabledConfig = pointer.To(true) + + return c + }, + }, + { + name: "invalid percent", + + cfg: func(t *testing.T) *block.ZswapConfigV1Alpha1 { + c := block.NewZswapConfigV1Alpha1() + c.MaxPoolPercentConfig = pointer.To(150) + + return c + }, + + expectedErrors: "maxPoolPercent must be between 0 and 100", + }, + } { + 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/resources/block/block.go b/pkg/machinery/resources/block/block.go index de6fa1cf025..bb235ffd5a6 100644 --- a/pkg/machinery/resources/block/block.go +++ b/pkg/machinery/resources/block/block.go @@ -19,7 +19,7 @@ import ( "github.com/siderolabs/talos/pkg/machinery/resources/v1alpha1" ) -//go:generate 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 -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go . +//go:generate 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 enumer -type=VolumeType,VolumePhase,FilesystemType,EncryptionKeyType,EncryptionProviderType -linecomment -text diff --git a/pkg/machinery/resources/block/block_test.go b/pkg/machinery/resources/block/block_test.go index 22312ff71bb..9e4ffcb5a5a 100644 --- a/pkg/machinery/resources/block/block_test.go +++ b/pkg/machinery/resources/block/block_test.go @@ -40,6 +40,7 @@ func TestRegisterResource(t *testing.T) { &block.VolumeMountRequest{}, &block.VolumeMountStatus{}, &block.VolumeStatus{}, + &block.ZswapStatus{}, } { assert.NoError(t, resourceRegistry.Register(ctx, resource)) } diff --git a/pkg/machinery/resources/block/deep_copy.generated.go b/pkg/machinery/resources/block/deep_copy.generated.go index f8835e785a5..47e458ae910 100644 --- a/pkg/machinery/resources/block/deep_copy.generated.go +++ b/pkg/machinery/resources/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 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 -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT. +// Code generated by "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 ."; DO NOT EDIT. package block @@ -148,3 +148,9 @@ func (o VolumeStatusSpec) DeepCopy() VolumeStatusSpec { } return cp } + +// DeepCopy generates a deep copy of ZswapStatusSpec. +func (o ZswapStatusSpec) DeepCopy() ZswapStatusSpec { + var cp ZswapStatusSpec = o + return cp +} diff --git a/pkg/machinery/resources/block/zswap_status.go b/pkg/machinery/resources/block/zswap_status.go new file mode 100644 index 00000000000..8e7d76ed5b9 --- /dev/null +++ b/pkg/machinery/resources/block/zswap_status.go @@ -0,0 +1,86 @@ +// 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 + +import ( + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "github.com/cosi-project/runtime/pkg/resource/protobuf" + "github.com/cosi-project/runtime/pkg/resource/typed" + + "github.com/siderolabs/talos/pkg/machinery/proto" +) + +// ZswapStatusType is type of ZswapStatus resource. +const ZswapStatusType = resource.Type("ZswapStatuses.block.talos.dev") + +// ZswapStatus resource holds status of zwap subsystem. +type ZswapStatus = typed.Resource[ZswapStatusSpec, ZswapStatusExtension] + +// ZswapStatusID is the ID of the singleton ZswapStatus resource. +const ZswapStatusID resource.ID = "zswap" + +// ZswapStatusSpec is the spec for ZswapStatus resource. +// +//gotagsrewrite:gen +type ZswapStatusSpec struct { + TotalSizeBytes uint64 `yaml:"totalSize" protobuf:"1"` + TotalSizeHuman string `yaml:"totalSizeHuman" protobuf:"2"` + StoredPages uint64 `yaml:"storedPages" protobuf:"3"` + PoolLimitHit uint64 `yaml:"poolLimitHit" protobuf:"4"` + RejectReclaimFail uint64 `yaml:"rejectReclaimFail" protobuf:"5"` + RejectAllocFail uint64 `yaml:"rejectAllocFail" protobuf:"6"` + RejectKmemcacheFail uint64 `yaml:"rejectKmemcacheFail" protobuf:"7"` + RejectCompressFail uint64 `yaml:"rejectCompressFail" protobuf:"8"` + RejectCompressPoor uint64 `yaml:"rejectCompressPoor" protobuf:"9"` + WrittenBackPages uint64 `yaml:"writtenBackPages" protobuf:"10"` +} + +// NewZswapStatus initializes a ZswapStatus resource. +func NewZswapStatus(namespace resource.Namespace, id resource.ID) *ZswapStatus { + return typed.NewResource[ZswapStatusSpec, ZswapStatusExtension]( + resource.NewMetadata(namespace, ZswapStatusType, id, resource.VersionUndefined), + ZswapStatusSpec{}, + ) +} + +// ZswapStatusExtension is auxiliary resource data for ZswapStatus. +type ZswapStatusExtension struct{} + +// ResourceDefinition implements meta.ResourceDefinitionProvider interface. +func (ZswapStatusExtension) ResourceDefinition() meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: ZswapStatusType, + Aliases: []resource.Type{}, + DefaultNamespace: NamespaceName, + PrintColumns: []meta.PrintColumn{ + { + Name: "Total Size", + JSONPath: "{.totalSizeHuman}", + }, + { + Name: "Stored Pages", + JSONPath: "{.storedPages}", + }, + { + Name: "Written Back", + JSONPath: "{.writtenBackPages}", + }, + { + Name: "Pool Limit Hit", + JSONPath: "{.poolLimitHit}", + }, + }, + } +} + +func init() { + proto.RegisterDefaultTypes() + + err := protobuf.RegisterDynamic[ZswapStatusSpec](ZswapStatusType, &ZswapStatus{}) + if err != nil { + panic(err) + } +} diff --git a/website/content/v1.11/reference/api.md b/website/content/v1.11/reference/api.md index 4980d8b098a..2c7c18daef8 100644 --- a/website/content/v1.11/reference/api.md +++ b/website/content/v1.11/reference/api.md @@ -51,6 +51,7 @@ description: Talos gRPC API reference. - [VolumeMountRequestSpec](#talos.resource.definitions.block.VolumeMountRequestSpec) - [VolumeMountStatusSpec](#talos.resource.definitions.block.VolumeMountStatusSpec) - [VolumeStatusSpec](#talos.resource.definitions.block.VolumeStatusSpec) + - [ZswapStatusSpec](#talos.resource.definitions.block.ZswapStatusSpec) - [resource/definitions/cluster/cluster.proto](#resource/definitions/cluster/cluster.proto) - [AffiliateSpec](#talos.resource.definitions.cluster.AffiliateSpec) @@ -1302,6 +1303,30 @@ VolumeStatusSpec is the spec for VolumeStatus resource. + + + +### ZswapStatusSpec +ZswapStatusSpec is the spec for ZswapStatus resource. + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| total_size_bytes | [uint64](#uint64) | | | +| total_size_human | [string](#string) | | | +| stored_pages | [uint64](#uint64) | | | +| pool_limit_hit | [uint64](#uint64) | | | +| reject_reclaim_fail | [uint64](#uint64) | | | +| reject_alloc_fail | [uint64](#uint64) | | | +| reject_kmemcache_fail | [uint64](#uint64) | | | +| reject_compress_fail | [uint64](#uint64) | | | +| reject_compress_poor | [uint64](#uint64) | | | +| written_back_pages | [uint64](#uint64) | | | + + + + + diff --git a/website/content/v1.11/reference/configuration/block/zswapconfig.md b/website/content/v1.11/reference/configuration/block/zswapconfig.md new file mode 100644 index 00000000000..5efa3c4d0fc --- /dev/null +++ b/website/content/v1.11/reference/configuration/block/zswapconfig.md @@ -0,0 +1,37 @@ +--- +description: | + ZswapConfig is a zswap (compressed memory) configuration document. + When zswap is enabled, Linux kernel compresses pages that would otherwise be swapped out to disk. + The compressed pages are stored in a memory pool, which is used to avoid writing to disk + when the system is under memory pressure. +title: ZswapConfig +--- + + + + + + + + + + + +{{< highlight yaml >}} +apiVersion: v1alpha1 +kind: ZswapConfig +maxPoolPercent: 25 # The maximum percent of memory that zswap can use. +shrinkerEnabled: true # Enable the shrinker feature: kernel might move +{{< /highlight >}} + + +| Field | Type | Description | Value(s) | +|-------|------|-------------|----------| +|`maxPoolPercent` |int |The maximum percent of memory that zswap can use.
This is a percentage of the total system memory.
The value must be between 0 and 100. | | +|`shrinkerEnabled` |bool |Enable the shrinker feature: kernel might move
cold pages from zswap to swap device to free up memory
for other use cases. | | + + + + + + diff --git a/website/content/v1.11/schemas/config.schema.json b/website/content/v1.11/schemas/config.schema.json index aa1de056032..3d259d8d792 100644 --- a/website/content/v1.11/schemas/config.schema.json +++ b/website/content/v1.11/schemas/config.schema.json @@ -370,6 +370,49 @@ ], "description": "VolumeConfig is a system volume configuration document.\\nNote: at the moment, only `EPHEMERAL` and `IMAGE-CACHE` system volumes are supported.\\n" }, + "block.ZswapConfigV1Alpha1": { + "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": [ + "ZswapConfig" + ], + "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" + }, + "maxPoolPercent": { + "type": "integer", + "title": "maxPoolPercent", + "description": "The maximum percent of memory that zswap can use.\nThis is a percentage of the total system memory.\nThe value must be between 0 and 100.\n", + "markdownDescription": "The maximum percent of memory that zswap can use.\nThis is a percentage of the total system memory.\nThe value must be between 0 and 100.", + "x-intellij-html-description": "\u003cp\u003eThe maximum percent of memory that zswap can use.\nThis is a percentage of the total system memory.\nThe value must be between 0 and 100.\u003c/p\u003e\n" + }, + "shrinkerEnabled": { + "type": "boolean", + "title": "shrinkerEnabled", + "description": "Enable the shrinker feature: kernel might move\ncold pages from zswap to swap device to free up memory\nfor other use cases.\n", + "markdownDescription": "Enable the shrinker feature: kernel might move\ncold pages from zswap to swap device to free up memory\nfor other use cases.", + "x-intellij-html-description": "\u003cp\u003eEnable the shrinker feature: kernel might move\ncold pages from zswap to swap device to free up memory\nfor other use cases.\u003c/p\u003e\n" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "apiVersion", + "kind" + ], + "description": "ZswapConfig is a zswap (compressed memory) configuration document.\\nWhen zswap is enabled, Linux kernel compresses pages that would otherwise be swapped out to disk.\\nThe compressed pages are stored in a memory pool, which is used to avoid writing to disk\\nwhen the system is under memory pressure.\\n" + }, "extensions.ConfigFile": { "properties": { "content": { @@ -4242,6 +4285,9 @@ { "$ref": "#/$defs/block.VolumeConfigV1Alpha1" }, + { + "$ref": "#/$defs/block.ZswapConfigV1Alpha1" + }, { "$ref": "#/$defs/extensions.ServiceConfigV1Alpha1" }, diff --git a/website/content/v1.11/talos-guides/configuration/swap.md b/website/content/v1.11/talos-guides/configuration/swap.md new file mode 100644 index 00000000000..82f8fc8e85a --- /dev/null +++ b/website/content/v1.11/talos-guides/configuration/swap.md @@ -0,0 +1,79 @@ +--- +title: "Swap" +description: "Guide on managing swap devices and zswap configuration in Talos Linux." +--- + +This guide provides an overview of the swap management features in Talos Linux. + +## Overview + +Swap devices are used to extend the available memory on a system by allowing the kernel to move inactive pages from RAM to disk. +Swap might be useful to free up memory when running memory-intensive workloads, but it can also lead to performance degradation if used excessively. +On other hand, moving inactive pages to swap can allow Linux to use more memory for buffers and caches, which can improve performance for workloads that benefit from caching. + +Zswap is a compressed cache for swap pages that can help reduce the performance impact of swapping by keeping frequently accessed pages in memory. +Swap and zswap can be used together, but they can also be configured independently. + +Swap and zswap are disabled by default in Talos, but can be enabled through the configuration. + +## Swap Devices + +Swap devices can be configured in the [Talos machine configuration]({{< relref "../../reference/configuration/block/swapvolumeconfig" >}}) similar to how [User Volumes]({{< relref "disk-management#" >}}) are configured. +As swap devices contain memory pages, it is recommended to enable disk encryption for swap devices to prevent sensitive data from being written to disk in plaintext. +It is also recommended to use a separate disk for swap devices to avoid performance degradation on the system disk and other workloads. + +For example, to configure a swap device on a NVMe disk of size 4GiB, using static key for encryption, the following configuration patch can be used: + +```yaml +apiVersion: v1alpha1 +kind: SwapVolumeConfig +name: swap1 +provisioning: + diskSelector: + match: 'disk.transport == "nvme"' + minSize: 4GiB + maxSize: 4GiB +encryption: + provider: luks2 + keys: + - slot: 0 + tpm: {} + - slot: 1 + static: + passphrase: topsecret +``` + +Talos Linux will automatically provision the partition on the disk, label it as `s-swap1`, encrypt it using the provided key, and enable it as a swap device. + +Current swap status can be checked using `talosctl get swap` command: + +```shell +$ talosctl -n 172.20.0.5 get swap +NODE NAMESPACE TYPE ID VERSION DEVICE SIZE USED PRIORITY +172.20.0.5 runtime SwapStatus /dev/nvme0n2p2 1 /dev/nvme0n2p2 3.9 GiB 100 MiB -2 +``` + +Removing a `SwapVolumeConfig` document will remove the swap device from the system, but the partition will remain on the disk. +To + +## Zswap + +Zswap is a compressed cache for swap pages that can help reduce the performance impact of swapping by keeping frequently accessed pages in memory. +Zswap can be enabled in the [Talos machine configuration]({{< relref "../../reference/configuration/block/zswapconfig" >}}): + +```yaml +apiVersion: v1alpha1 +kind: ZswapConfig +maxPoolPercent: 20 +``` + +This configuration enables zswap with a maximum pool size of 20% of the total system memory. +To check the current zswap status, you can use the `talosctl get zswapstatus` command: + +```shell +$ talosctl -n 172.20.0.5 get zswapstatus +NODE NAMESPACE TYPE ID VERSION TOTAL SIZE STORED PAGES WRITTEN BACK POOL LIMIT HIT +172.20.0.5 runtime ZswapStatus zswap 1 0 B 0 0 0 +``` + +Removing a `ZswapConfig` document will disable zswap on the system.