feat: Add support for configuring swap in Podman machine

Add `--swap` argument to `podman machine init` command.

Passing an int64 value to this flag will trigger the Podman machine
ignition file to be generated with a zram-generator.conf file containing
the --swap value as the zram-size argument.

This file is read by the zram-generator systemd service on boot
resulting in a zram swap device being created.

Fixes: https://github.com/containers/podman/issues/15980

Signed-off-by: Lewis Roy <lewis@redhat.com>
This commit is contained in:
Lewis Roy 2025-04-22 22:42:35 +10:00
parent ce0bac24e5
commit 7b1055a5fb
No known key found for this signature in database
13 changed files with 81 additions and 17 deletions

View File

@ -83,6 +83,14 @@ func init() {
) )
_ = initCmd.RegisterFlagCompletionFunc(memoryFlagName, completion.AutocompleteNone) _ = initCmd.RegisterFlagCompletionFunc(memoryFlagName, completion.AutocompleteNone)
swapFlagName := "swap"
flags.Uint64VarP(
&initOpts.Swap,
swapFlagName, "s", 0,
"Swap in MiB",
)
_ = initCmd.RegisterFlagCompletionFunc(swapFlagName, completion.AutocompleteNone)
flags.BoolVar( flags.BoolVar(
&now, &now,
"now", false, "now", false,

View File

@ -119,6 +119,7 @@ func outputTemplate(cmd *cobra.Command, responses []*entities.ListReporter) erro
"CPUs": "CPUS", "CPUs": "CPUS",
"Memory": "MEMORY", "Memory": "MEMORY",
"DiskSize": "DISK SIZE", "DiskSize": "DISK SIZE",
"Swap": "SWAP",
}) })
rpt := report.New(os.Stdout, cmd.Name()) rpt := report.New(os.Stdout, cmd.Name())
@ -182,6 +183,7 @@ func toMachineFormat(vms []*machine.ListResponse, defaultCon *config.Connection)
response.VMType = vm.VMType response.VMType = vm.VMType
response.CPUs = vm.CPUs response.CPUs = vm.CPUs
response.Memory = strUint(uint64(vm.Memory.ToBytes())) response.Memory = strUint(uint64(vm.Memory.ToBytes()))
response.Swap = strUint(uint64(vm.Swap.ToBytes()))
response.DiskSize = strUint(uint64(vm.DiskSize.ToBytes())) response.DiskSize = strUint(uint64(vm.DiskSize.ToBytes()))
response.Port = vm.Port response.Port = vm.Port
response.RemoteUsername = vm.RemoteUsername response.RemoteUsername = vm.RemoteUsername
@ -225,6 +227,7 @@ func toHumanFormat(vms []*machine.ListResponse, defaultCon *config.Connection) [
response.VMType = vm.VMType response.VMType = vm.VMType
response.CPUs = vm.CPUs response.CPUs = vm.CPUs
response.Memory = units.BytesSize(float64(vm.Memory.ToBytes())) response.Memory = units.BytesSize(float64(vm.Memory.ToBytes()))
response.Swap = units.BytesSize(float64(vm.Swap.ToBytes()))
response.DiskSize = units.BytesSize(float64(vm.DiskSize.ToBytes())) response.DiskSize = units.BytesSize(float64(vm.DiskSize.ToBytes()))
humanResponses = append(humanResponses, response) humanResponses = append(humanResponses, response)

View File

@ -104,6 +104,12 @@ if there is no existing remote connection configurations.
API forwarding, if available, follows this setting. API forwarding, if available, follows this setting.
#### **--swap**, **-s**=*number*
Swap (in MiB). Note: 1024MiB = 1GiB.
Renders a `zram-generator.conf` file with zram-size set to the value passed to --swap
#### **--timezone** #### **--timezone**
Set the timezone for the machine and containers. Valid values are `local` or Set the timezone for the machine and containers. Valid values are `local` or

View File

@ -50,6 +50,7 @@ Valid placeholders for the Go template are listed below:
| .RemoteUsername | VM Username for rootless Podman | | .RemoteUsername | VM Username for rootless Podman |
| .Running | Is machine running | | .Running | Is machine running |
| .Stream | Stream name | | .Stream | Stream name |
| .Swap | Allocated swap for machine |
| .UserModeNetworking | Whether machine uses user-mode networking | | .UserModeNetworking | Whether machine uses user-mode networking |
| .VMType | VM type | | .VMType | VM type |

View File

@ -13,6 +13,7 @@ type ListReporter struct {
VMType string VMType string
CPUs uint64 CPUs uint64
Memory string Memory string
Swap string
DiskSize string DiskSize string
Port int Port int
RemoteUsername string RemoteUsername string

View File

@ -22,9 +22,7 @@ import (
const apiUpTimeout = 20 * time.Second const apiUpTimeout = 20 * time.Second
var ( var ForwarderBinaryName = "gvproxy"
ForwarderBinaryName = "gvproxy"
)
type Download struct { type Download struct {
Arch string Arch string
@ -55,6 +53,7 @@ type ListResponse struct {
VMType string VMType string
CPUs uint64 CPUs uint64
Memory strongunits.MiB Memory strongunits.MiB
Swap strongunits.MiB
DiskSize strongunits.GiB DiskSize strongunits.GiB
Port int Port int
RemoteUsername string RemoteUsername string

View File

@ -11,6 +11,7 @@ type InitOptions struct {
Volumes []string Volumes []string
IsDefault bool IsDefault bool
Memory uint64 Memory uint64
Swap uint64
Name string Name string
TimeZone string TimeZone string
URI url.URL URI url.URL

View File

@ -28,6 +28,7 @@ type initMachine struct {
playbook string playbook string
cpus *uint cpus *uint
diskSize *uint diskSize *uint
swap *uint
ignitionPath string ignitionPath string
username string username string
image string image string
@ -81,6 +82,9 @@ func (i *initMachine) buildCmd(m *machineTestBuilder) []string {
if i.userModeNetworking { if i.userModeNetworking {
cmd = append(cmd, "--user-mode-networking") cmd = append(cmd, "--user-mode-networking")
} }
if i.swap != nil {
cmd = append(cmd, "--swap", strconv.Itoa(int(*i.swap)))
}
name := m.name name := m.name
cmd = append(cmd, name) cmd = append(cmd, name)
@ -112,11 +116,17 @@ func (i *initMachine) withCPUs(num uint) *initMachine {
i.cpus = &num i.cpus = &num
return i return i
} }
func (i *initMachine) withDiskSize(size uint) *initMachine { func (i *initMachine) withDiskSize(size uint) *initMachine {
i.diskSize = &size i.diskSize = &size
return i return i
} }
func (i *initMachine) withSwap(size uint) *initMachine {
i.swap = &size
return i
}
func (i *initMachine) withIgnitionPath(path string) *initMachine { func (i *initMachine) withIgnitionPath(path string) *initMachine {
i.ignitionPath = path i.ignitionPath = path
return i return i

View File

@ -238,7 +238,6 @@ var _ = Describe("podman machine init", func() {
Expect(testMachine.Resources.Memory).To(BeEquivalentTo(uint64(2048))) Expect(testMachine.Resources.Memory).To(BeEquivalentTo(uint64(2048)))
} }
Expect(testMachine.SSHConfig.RemoteUsername).To(Equal(remoteUsername)) Expect(testMachine.SSHConfig.RemoteUsername).To(Equal(remoteUsername))
}) })
It("machine init with cpus, disk size, memory, timezone", func() { It("machine init with cpus, disk size, memory, timezone", func() {
@ -282,6 +281,23 @@ var _ = Describe("podman machine init", func() {
Expect(timezoneSession.outputToString()).To(ContainSubstring("HST")) Expect(timezoneSession.outputToString()).To(ContainSubstring("HST"))
}) })
It("machine init with swap", func() {
skipIfWSL("Configuring swap is not supported on WSL")
name := randomString()
i := new(initMachine)
session, err := mb.setName(name).setCmd(i.withImage(mb.imagePath).withSwap(2048).withNow()).run()
Expect(err).ToNot(HaveOccurred())
Expect(session).To(Exit(0))
ssh := &sshMachine{}
sshSession, err := mb.setName(name).setCmd(ssh.withSSHCommand([]string{"zramctl -bo DISKSIZE"})).run()
Expect(err).ToNot(HaveOccurred())
Expect(sshSession).To(Exit(0))
// 2147483648 bytes = 2048MiB
Expect(sshSession.outputToString()).To(ContainSubstring("2147483648"))
})
It("machine init with volume", func() { It("machine init with volume", func() {
if testProvider.VMType() == define.HyperVVirt { if testProvider.VMType() == define.HyperVVirt {
Skip("volumes are not supported on hyperv yet") Skip("volumes are not supported on hyperv yet")
@ -373,7 +389,6 @@ var _ = Describe("podman machine init", func() {
output := strings.TrimSpace(sshSession2.outputToString()) output := strings.TrimSpace(sshSession2.outputToString())
Expect(output).To(HavePrefix("/run/user")) Expect(output).To(HavePrefix("/run/user"))
Expect(output).To(HaveSuffix("/podman/podman.sock")) Expect(output).To(HaveSuffix("/podman/podman.sock"))
}) })
It("machine init rootful with docker.sock check", func() { It("machine init rootful with docker.sock check", func() {

View File

@ -67,6 +67,7 @@ type DynamicIgnition struct {
Rootful bool Rootful bool
NetRecover bool NetRecover bool
Rosetta bool Rosetta bool
Swap uint64
} }
func (ign *DynamicIgnition) Write() error { func (ign *DynamicIgnition) Write() error {
@ -136,7 +137,7 @@ func (ign *DynamicIgnition) GenerateIgnitionConfig() error {
ignStorage := Storage{ ignStorage := Storage{
Directories: getDirs(ign.Name), Directories: getDirs(ign.Name),
Files: getFiles(ign.Name, ign.UID, ign.Rootful, ign.VMType, ign.NetRecover), Files: getFiles(ign.Name, ign.UID, ign.Rootful, ign.VMType, ign.NetRecover, ign.Swap),
Links: getLinks(ign.Name), Links: getLinks(ign.Name),
} }
@ -293,7 +294,7 @@ func getDirs(usrName string) []Directory {
return dirs return dirs
} }
func getFiles(usrName string, uid int, rootful bool, vmtype define.VMType, _ bool) []File { func getFiles(usrName string, uid int, rootful bool, vmtype define.VMType, _ bool, swap uint64) []File {
files := make([]File, 0) files := make([]File, 0)
lingerExample := parser.NewUnitFile() lingerExample := parser.NewUnitFile()
@ -407,6 +408,21 @@ pids_limit=0
}, },
}) })
if swap > 0 {
files = append(files, File{
Node: Node{
Path: "/etc/systemd/zram-generator.conf",
},
FileEmbedded1: FileEmbedded1{
Append: nil,
Contents: Resource{
Source: EncodeDataURLPtr(fmt.Sprintf("[zram0]\nzram-size=%d\n", swap)),
},
Mode: IntToPtr(0644),
},
})
}
// get certs for current user // get certs for current user
userHome, err := os.UserHomeDir() userHome, err := os.UserHomeDir()
if err != nil { if err != nil {

View File

@ -30,9 +30,7 @@ import (
// List is done at the host level to allow for a *possible* future where // List is done at the host level to allow for a *possible* future where
// more than one provider is used // more than one provider is used
func List(vmstubbers []vmconfigs.VMProvider, _ machine.ListOptions) ([]*machine.ListResponse, error) { func List(vmstubbers []vmconfigs.VMProvider, _ machine.ListOptions) ([]*machine.ListResponse, error) {
var ( var lrs []*machine.ListResponse
lrs []*machine.ListResponse
)
for _, s := range vmstubbers { for _, s := range vmstubbers {
dirs, err := env.GetMachineDirs(s.VMType()) dirs, err := env.GetMachineDirs(s.VMType())
@ -54,10 +52,10 @@ func List(vmstubbers []vmconfigs.VMProvider, _ machine.ListOptions) ([]*machine.
LastUp: mc.LastUp, LastUp: mc.LastUp,
Running: state == machineDefine.Running, Running: state == machineDefine.Running,
Starting: mc.Starting, Starting: mc.Starting,
//Stream: "", // No longer applicable
VMType: s.VMType().String(), VMType: s.VMType().String(),
CPUs: mc.Resources.CPUs, CPUs: mc.Resources.CPUs,
Memory: mc.Resources.Memory, Memory: mc.Resources.Memory,
Swap: mc.Swap,
DiskSize: mc.Resources.DiskSize, DiskSize: mc.Resources.DiskSize,
Port: mc.SSH.Port, Port: mc.SSH.Port,
RemoteUsername: mc.SSH.RemoteUsername, RemoteUsername: mc.SSH.RemoteUsername,
@ -204,13 +202,13 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) error {
VMType: mp.VMType(), VMType: mp.VMType(),
WritePath: ignitionFile.GetPath(), WritePath: ignitionFile.GetPath(),
Rootful: opts.Rootful, Rootful: opts.Rootful,
Swap: opts.Swap,
}) })
// If the user provides an ignition file, we need to // If the user provides an ignition file, we need to
// copy it into the conf dir // copy it into the conf dir
if len(opts.IgnitionPath) > 0 { if len(opts.IgnitionPath) > 0 {
err = ignBuilder.BuildWithIgnitionFile(opts.IgnitionPath) err = ignBuilder.BuildWithIgnitionFile(opts.IgnitionPath)
if err != nil { if err != nil {
return err return err
} }

View File

@ -27,6 +27,8 @@ type MachineConfig struct {
SSH SSHConfig SSH SSHConfig
Version uint Version uint
Swap strongunits.MiB
// Image stuff // Image stuff
imageDescription machineImage //nolint:unused imageDescription machineImage //nolint:unused

View File

@ -81,6 +81,10 @@ func NewMachineConfig(opts define.InitOptions, dirs *define.MachineDirs, sshIden
} }
mc.Resources = mrc mc.Resources = mrc
if opts.Swap > 0 {
mc.Swap = strongunits.MiB(opts.Swap)
}
sshPort, err := ports.AllocateMachinePort() sshPort, err := ports.AllocateMachinePort()
if err != nil { if err != nil {
return nil, err return nil, err