Skip to content

Commit e1ae7bf

Browse files
authored
qemu: reduce monitor socket path (#13971)
The QEMU driver can take an optional `graceful_shutdown` configuration which will create a Unix socket to send ACPI shutdown signal to the VM. Unix sockets have a hard length limit and the driver implementation assumed that QEMU versions 2.10.1 were able to handle longer paths. This is not correct, the linked QEMU fix only changed the behaviour from silently truncating longer socket paths to throwing an error. By validating the socket path before starting the QEMU machine we can provide users a more actionable and meaningful error message, and by using a shorter socket file name we leave a bit more room for user-defined values in the path, such as the task name. The maximum length allowed is also platform-dependant, so validation needs to be different for each OS.
1 parent e5e8490 commit e1ae7bf

File tree

9 files changed

+82
-183
lines changed

9 files changed

+82
-183
lines changed

.changelog/13971.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:improvement
2+
qemu: use shorter socket file names to reduce the chance of hitting the max path length
3+
```

drivers/qemu/driver.go

+38-49
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"strings"
1414
"time"
1515

16-
"github.com/coreos/go-semver/semver"
1716
hclog "github.com/hashicorp/go-hclog"
1817
"github.com/hashicorp/nomad/client/taskenv"
1918
"github.com/hashicorp/nomad/drivers/shared/eventer"
@@ -39,14 +38,13 @@ const (
3938

4039
// Represents an ACPI shutdown request to the VM (emulates pressing a physical power button)
4140
// Reference: https://en.wikibooks.org/wiki/QEMU/Monitor
41+
// Use a short file name since socket paths have a maximum length.
4242
qemuGracefulShutdownMsg = "system_powerdown\n"
43-
qemuMonitorSocketName = "qemu-monitor.sock"
43+
qemuMonitorSocketName = "qm.sock"
4444

4545
// Socket file enabling communication with the Qemu Guest Agent (if enabled and running)
46-
qemuGuestAgentSocketName = "qemu-guest-agent.sock"
47-
48-
// Maximum socket path length prior to qemu 2.10.1
49-
qemuLegacyMaxMonitorPathLen = 108
46+
// Use a short file name since socket paths have a maximum length.
47+
qemuGuestAgentSocketName = "qa.sock"
5048

5149
// taskHandleVersion is the version of task handle which this driver sets
5250
// and understands how to decode driver state
@@ -70,14 +68,6 @@ var (
7068

7169
versionRegex = regexp.MustCompile(`version (\d[\.\d+]+)`)
7270

73-
// Prior to qemu 2.10.1, monitor socket paths are truncated to 108 bytes.
74-
// We should consider this if driver.qemu.version is < 2.10.1 and the
75-
// generated monitor path is too long.
76-
//
77-
// Relevant fix is here:
78-
// https://github.com/qemu/qemu/commit/ad9579aaa16d5b385922d49edac2c96c79bcfb6
79-
qemuVersionLongSocketPathFix = semver.New("2.10.1")
80-
8171
// pluginInfo is the response returned for the PluginInfo RPC
8272
pluginInfo = &base.PluginInfoResponse{
8373
Type: base.PluginTypeDriver,
@@ -307,11 +297,19 @@ func (d *Driver) RecoverTask(handle *drivers.TaskHandle) error {
307297

308298
// Try to restore monitor socket path.
309299
taskDir := filepath.Join(handle.Config.AllocDir, handle.Config.Name)
310-
monitorPath := filepath.Join(taskDir, qemuMonitorSocketName)
311-
if _, err := os.Stat(monitorPath); err == nil {
312-
d.logger.Debug("found existing monitor socket", "monitor", monitorPath)
313-
} else {
314-
monitorPath = ""
300+
possiblePaths := []string{
301+
filepath.Join(taskDir, qemuMonitorSocketName),
302+
// Support restoring tasks that used the old socket name.
303+
filepath.Join(taskDir, "qemu-monitor.sock"),
304+
}
305+
306+
var monitorPath string
307+
for _, path := range possiblePaths {
308+
if _, err := os.Stat(path); err == nil {
309+
monitorPath = path
310+
d.logger.Debug("found existing monitor socket", "monitor", monitorPath)
311+
break
312+
}
315313
}
316314

317315
h := &taskHandle{
@@ -468,21 +466,17 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive
468466
}
469467
}
470468

469+
taskDir := filepath.Join(cfg.AllocDir, cfg.Name)
470+
471471
var monitorPath string
472472
if driverConfig.GracefulShutdown {
473473
if runtime.GOOS == "windows" {
474474
return nil, nil, errors.New("QEMU graceful shutdown is unsupported on the Windows platform")
475475
}
476476
// This socket will be used to manage the virtual machine (for example,
477477
// to perform graceful shutdowns)
478-
taskDir := filepath.Join(cfg.AllocDir, cfg.Name)
479-
fingerPrint := d.buildFingerprint()
480-
if fingerPrint.Attributes == nil {
481-
return nil, nil, fmt.Errorf("unable to get qemu driver version from fingerprinted attributes")
482-
}
483-
monitorPath, err = d.getMonitorPath(taskDir, fingerPrint)
484-
if err != nil {
485-
d.logger.Debug("could not get qemu monitor path", "error", err)
478+
monitorPath = filepath.Join(taskDir, qemuMonitorSocketName)
479+
if err := validateSocketPath(monitorPath); err != nil {
486480
return nil, nil, err
487481
}
488482
d.logger.Debug("got monitor path", "monitorPath", monitorPath)
@@ -494,8 +488,12 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive
494488
return nil, nil, errors.New("QEMU Guest Agent socket is unsupported on the Windows platform")
495489
}
496490
// This socket will be used to communicate with the Guest Agent (if it's running)
497-
taskDir := filepath.Join(cfg.AllocDir, cfg.Name)
498-
args = append(args, "-chardev", fmt.Sprintf("socket,path=%s/%s,server,nowait,id=qga0", taskDir, qemuGuestAgentSocketName))
491+
agentSocketPath := filepath.Join(taskDir, qemuGuestAgentSocketName)
492+
if err := validateSocketPath(agentSocketPath); err != nil {
493+
return nil, nil, err
494+
}
495+
496+
args = append(args, "-chardev", fmt.Sprintf("socket,path=%s,server,nowait,id=qga0", agentSocketPath))
499497
args = append(args, "-device", "virtio-serial")
500498
args = append(args, "-device", "virtserialport,chardev=qga0,name=org.qemu.guest_agent.0")
501499
}
@@ -647,6 +645,8 @@ func (d *Driver) StopTask(taskID string, timeout time.Duration, signal string) e
647645
if err := sendQemuShutdown(d.logger, handle.monitorPath, handle.pid); err != nil {
648646
d.logger.Debug("error sending graceful shutdown ", "pid", handle.pid, "error", err)
649647
}
648+
} else {
649+
d.logger.Debug("monitor socket is empty, forcing shutdown")
650650
}
651651

652652
// TODO(preetha) we are calling shutdown on the executor here
@@ -748,27 +748,16 @@ func (d *Driver) handleWait(ctx context.Context, handle *taskHandle, ch chan *dr
748748
}
749749
}
750750

751-
// getMonitorPath is used to determine whether a qemu monitor socket can be
752-
// safely created and accessed in the task directory by the version of qemu
753-
// present on the host. If it is safe to use, the socket's full path is
754-
// returned along with a nil error. Otherwise, an empty string is returned
755-
// along with a descriptive error.
756-
func (d *Driver) getMonitorPath(dir string, fingerPrint *drivers.Fingerprint) (string, error) {
757-
var longPathSupport bool
758-
currentQemuVer := fingerPrint.Attributes[driverVersionAttr]
759-
currentQemuSemver := semver.New(currentQemuVer.GoString())
760-
if currentQemuSemver.LessThan(*qemuVersionLongSocketPathFix) {
761-
longPathSupport = false
762-
d.logger.Debug("long socket paths are not available in this version of QEMU", "version", currentQemuVer)
763-
} else {
764-
longPathSupport = true
765-
d.logger.Debug("long socket paths available in this version of QEMU", "version", currentQemuVer)
766-
}
767-
fullSocketPath := fmt.Sprintf("%s/%s", dir, qemuMonitorSocketName)
768-
if len(fullSocketPath) > qemuLegacyMaxMonitorPathLen && !longPathSupport {
769-
return "", fmt.Errorf("monitor path is too long for this version of qemu")
751+
// validateSocketPath provides best effort validation of socket paths since
752+
// some rules may be platform-dependant.
753+
func validateSocketPath(path string) error {
754+
if maxSocketPathLen > 0 && len(path) > maxSocketPathLen {
755+
return fmt.Errorf(
756+
"socket path %s is longer than the maximum length allowed (%d), try to reduce the task name or Nomad's data_dir if possible.",
757+
path, maxSocketPathLen)
770758
}
771-
return fullSocketPath, nil
759+
760+
return nil
772761
}
773762

774763
// sendQemuShutdown attempts to issue an ACPI power-off command via the qemu

drivers/qemu/driver_bsd.go

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//go:build darwin || freebsd || netbsd || openbsd
2+
// +build darwin freebsd netbsd openbsd
3+
4+
package qemu
5+
6+
const (
7+
// https://man.openbsd.org/unix.4#ADDRESSING
8+
// https://www.freebsd.org/cgi/man.cgi?query=unix
9+
// https://github.com/apple/darwin-xnu/blob/main/bsd/man/man4/unix.4#L72
10+
// https://man.netbsd.org/unix.4#ADDRESSING
11+
maxSocketPathLen = 104
12+
)

drivers/qemu/driver_fallback.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//go:build !linux && !darwin && !freebsd && !netbsd && !openbsd
2+
// +build !linux,!darwin,!freebsd,!netbsd,!openbsd
3+
4+
package qemu
5+
6+
const (
7+
// Don't enforce any path limit.
8+
maxSocketPathLen = 0
9+
)

drivers/qemu/driver_linux.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//go:build linux
2+
// +build linux
3+
4+
package qemu
5+
6+
const (
7+
// https://man7.org/linux/man-pages/man7/unix.7.html
8+
maxSocketPathLen = 108
9+
)

drivers/qemu/driver_test.go

+1-116
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"io"
66
"os"
77
"path/filepath"
8-
"strings"
98
"testing"
109
"time"
1110

@@ -17,7 +16,6 @@ import (
1716
"github.com/hashicorp/nomad/nomad/structs"
1817
"github.com/hashicorp/nomad/plugins/drivers"
1918
dtestutil "github.com/hashicorp/nomad/plugins/drivers/testutils"
20-
pstructs "github.com/hashicorp/nomad/plugins/shared/structs"
2119
"github.com/hashicorp/nomad/testutil"
2220
"github.com/stretchr/testify/require"
2321
)
@@ -88,119 +86,6 @@ func TestQemuDriver_Start_Wait_Stop(t *testing.T) {
8886

8987
}
9088

91-
// Verifies monitor socket path for old qemu
92-
func TestQemuDriver_GetMonitorPathOldQemu(t *testing.T) {
93-
ci.Parallel(t)
94-
ctestutil.QemuCompatible(t)
95-
96-
require := require.New(t)
97-
ctx, cancel := context.WithCancel(context.Background())
98-
defer cancel()
99-
100-
d := NewQemuDriver(ctx, testlog.HCLogger(t))
101-
harness := dtestutil.NewDriverHarness(t, d)
102-
103-
task := &drivers.TaskConfig{
104-
ID: uuid.Generate(),
105-
Name: "linux",
106-
Resources: &drivers.Resources{
107-
NomadResources: &structs.AllocatedTaskResources{
108-
Memory: structs.AllocatedMemoryResources{
109-
MemoryMB: 512,
110-
},
111-
Cpu: structs.AllocatedCpuResources{
112-
CpuShares: 100,
113-
},
114-
Networks: []*structs.NetworkResource{
115-
{
116-
ReservedPorts: []structs.Port{{Label: "main", Value: 22000}, {Label: "web", Value: 80}},
117-
},
118-
},
119-
},
120-
},
121-
}
122-
123-
cleanup := harness.MkAllocDir(task, true)
124-
defer cleanup()
125-
126-
fingerPrint := &drivers.Fingerprint{
127-
Attributes: map[string]*pstructs.Attribute{
128-
driverVersionAttr: pstructs.NewStringAttribute("2.0.0"),
129-
},
130-
}
131-
shortPath := strings.Repeat("x", 10)
132-
qemuDriver := d.(*Driver)
133-
_, err := qemuDriver.getMonitorPath(shortPath, fingerPrint)
134-
require.Nil(err)
135-
136-
longPath := strings.Repeat("x", qemuLegacyMaxMonitorPathLen+100)
137-
_, err = qemuDriver.getMonitorPath(longPath, fingerPrint)
138-
require.NotNil(err)
139-
140-
// Max length includes the '/' separator and socket name
141-
maxLengthCount := qemuLegacyMaxMonitorPathLen - len(qemuMonitorSocketName) - 1
142-
maxLengthLegacyPath := strings.Repeat("x", maxLengthCount)
143-
_, err = qemuDriver.getMonitorPath(maxLengthLegacyPath, fingerPrint)
144-
require.Nil(err)
145-
}
146-
147-
// Verifies monitor socket path for new qemu version
148-
func TestQemuDriver_GetMonitorPathNewQemu(t *testing.T) {
149-
ci.Parallel(t)
150-
ctestutil.QemuCompatible(t)
151-
152-
require := require.New(t)
153-
ctx, cancel := context.WithCancel(context.Background())
154-
defer cancel()
155-
156-
d := NewQemuDriver(ctx, testlog.HCLogger(t))
157-
harness := dtestutil.NewDriverHarness(t, d)
158-
159-
task := &drivers.TaskConfig{
160-
ID: uuid.Generate(),
161-
Name: "linux",
162-
Resources: &drivers.Resources{
163-
NomadResources: &structs.AllocatedTaskResources{
164-
Memory: structs.AllocatedMemoryResources{
165-
MemoryMB: 512,
166-
},
167-
Cpu: structs.AllocatedCpuResources{
168-
CpuShares: 100,
169-
},
170-
Networks: []*structs.NetworkResource{
171-
{
172-
ReservedPorts: []structs.Port{{Label: "main", Value: 22000}, {Label: "web", Value: 80}},
173-
},
174-
},
175-
},
176-
},
177-
}
178-
179-
cleanup := harness.MkAllocDir(task, true)
180-
defer cleanup()
181-
182-
fingerPrint := &drivers.Fingerprint{
183-
Attributes: map[string]*pstructs.Attribute{
184-
driverVersionAttr: pstructs.NewStringAttribute("2.99.99"),
185-
},
186-
}
187-
shortPath := strings.Repeat("x", 10)
188-
qemuDriver := d.(*Driver)
189-
_, err := qemuDriver.getMonitorPath(shortPath, fingerPrint)
190-
require.Nil(err)
191-
192-
// Should not return an error in this qemu version
193-
longPath := strings.Repeat("x", qemuLegacyMaxMonitorPathLen+100)
194-
_, err = qemuDriver.getMonitorPath(longPath, fingerPrint)
195-
require.Nil(err)
196-
197-
// Max length includes the '/' separator and socket name
198-
maxLengthCount := qemuLegacyMaxMonitorPathLen - len(qemuMonitorSocketName) - 1
199-
maxLengthLegacyPath := strings.Repeat("x", maxLengthCount)
200-
_, err = qemuDriver.getMonitorPath(maxLengthLegacyPath, fingerPrint)
201-
require.Nil(err)
202-
}
203-
20489
// copyFile moves an existing file to the destination
20590
func copyFile(src, dst string, t *testing.T) {
20691
in, err := os.Open(src)
@@ -464,7 +349,7 @@ func TestIsAllowedImagePath(t *testing.T) {
464349

465350
func TestArgsAllowList(t *testing.T) {
466351
ci.Parallel(t)
467-
352+
468353
pluginConfigAllowList := []string{"-drive", "-net", "-snapshot"}
469354

470355
validArgs := [][]string{

go.mod

-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ require (
2323
github.com/containernetworking/cni v1.0.1
2424
github.com/containernetworking/plugins v1.0.1
2525
github.com/coreos/go-iptables v0.6.0
26-
github.com/coreos/go-semver v0.3.0
2726
github.com/creack/pty v1.1.18
2827
github.com/docker/cli v20.10.3-0.20220113150236-6e2838e18645+incompatible
2928
github.com/docker/distribution v2.8.1+incompatible

go.sum

-1
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,6 @@ github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo
352352
github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
353353
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
354354
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
355-
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
356355
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
357356
github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
358357
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=

website/content/docs/drivers/qemu.mdx

+10-16
Original file line numberDiff line numberDiff line change
@@ -55,22 +55,16 @@ The `qemu` driver supports the following configuration in the job spec:
5555
signal to virtual machines rather than simply terminating them. This emulates
5656
a physical power button press, and gives instances a chance to shut down
5757
cleanly. If the VM is still running after `kill_timeout`, it will be
58-
forcefully terminated. (Note that
59-
[prior to qemu 2.10.1](https://github.com/qemu/qemu/commit/ad9579aaa16d5b385922d49edac2c96c79bcfb6),
60-
the monitor socket path is limited to 108 characters. Graceful shutdown will
61-
be disabled if QEMU is < 2.10.1 and the generated monitor path exceeds this
62-
length. You may encounter this issue if you set long
63-
[data_dir](/docs/configuration#data_dir)
64-
or
65-
[alloc_dir](/docs/configuration/client#alloc_dir)
66-
paths.) This feature is currently not supported on Windows.
67-
68-
- `guest_agent` `(bool: false)` - Enable support for the [QEMU Guest
69-
Agent](https://wiki.qemu.org/Features/GuestAgent) for this virtual machine. This
70-
will add the necessary virtual hardware and create a `qemu-guest-agent.sock`
71-
file in the task's working directory for interacting with the agent. The QEMU Guest
72-
Agent must be running in the guest VM. This feature is currently not supported
73-
on Windows.
58+
forcefully terminated. This feature uses a Unix socket that is placed within
59+
the task directory and operating systems may impose a limit on how long these
60+
paths can be. This feature is currently not supported on Windows.
61+
62+
- `guest_agent` `(bool: false)` - Enable support for the [QEMU Guest
63+
Agent](https://wiki.qemu.org/Features/GuestAgent) for this virtual machine.
64+
This will add the necessary virtual hardware and create a `qa.sock` file in
65+
the task's working directory for interacting with the agent. The QEMU Guest
66+
Agent must be running in the guest VM. This feature is currently not
67+
supported on Windows.
7468

7569
- `port_map` - (Optional) A key-value map of port labels.
7670

0 commit comments

Comments
 (0)