-
Notifications
You must be signed in to change notification settings - Fork 2.4k
/
machine.go
339 lines (303 loc) · 8.81 KB
/
machine.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
//go:build linux || freebsd || windows
package qemu
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
"github.com/containers/common/pkg/config"
"github.com/containers/podman/v5/pkg/errorhandling"
"github.com/containers/podman/v5/pkg/machine/define"
"github.com/containers/podman/v5/pkg/machine/vmconfigs"
"github.com/containers/storage/pkg/fileutils"
"github.com/digitalocean/go-qemu/qmp"
"github.com/sirupsen/logrus"
)
func NewStubber() (*QEMUStubber, error) {
return &QEMUStubber{}, nil
}
// qemuPid returns -1 or the PID of the running QEMU instance.
func qemuPid(pidFile *define.VMFile) (int, error) {
pidData, err := os.ReadFile(pidFile.GetPath())
if err != nil {
// The file may not yet exist on start or have already been
// cleaned up after stop, so we need to be defensive.
if errors.Is(err, os.ErrNotExist) {
return -1, nil
}
return -1, err
}
if len(pidData) == 0 {
return -1, nil
}
pid, err := strconv.Atoi(strings.TrimRight(string(pidData), "\n"))
if err != nil {
logrus.Warnf("Reading QEMU pidfile: %v", err)
return -1, nil
}
return findProcess(pid)
}
// todo move this to qemumonitor stuff. it has no use as a method of stubber
func (q *QEMUStubber) checkStatus(monitor *qmp.SocketMonitor) (define.Status, error) {
// this is the format returned from the monitor
// {"return": {"status": "running", "singlestep": false, "running": true}}
type statusDetails struct {
Status string `json:"status"`
Step bool `json:"singlestep"`
Running bool `json:"running"`
Starting bool `json:"starting"`
}
type statusResponse struct {
Response statusDetails `json:"return"`
}
var response statusResponse
checkCommand := struct {
Execute string `json:"execute"`
}{
Execute: "query-status",
}
input, err := json.Marshal(checkCommand)
if err != nil {
return "", err
}
b, err := monitor.Run(input)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return define.Stopped, nil
}
return "", err
}
if err := json.Unmarshal(b, &response); err != nil {
return "", err
}
if response.Response.Status == define.Running {
return define.Running, nil
}
return define.Stopped, nil
}
// waitForMachineToStop waits for the machine to stop running
func (q *QEMUStubber) waitForMachineToStop(mc *vmconfigs.MachineConfig) error {
fmt.Println("Waiting for VM to stop running...")
waitInternal := 250 * time.Millisecond
for i := 0; i < 5; i++ {
state, err := q.State(mc, false)
if err != nil {
return err
}
if state != define.Running {
break
}
time.Sleep(waitInternal)
waitInternal *= 2
}
// after the machine stops running it normally takes about 1 second for the
// qemu VM to exit so we wait a bit to try to avoid issues
time.Sleep(2 * time.Second)
return nil
}
// Stop uses the qmp monitor to call a system_powerdown
func (q *QEMUStubber) StopVM(mc *vmconfigs.MachineConfig, _ bool) error {
if err := mc.Refresh(); err != nil {
return err
}
stopErr := q.stopLocked(mc)
// Make sure that the associated QEMU process gets killed in case it's
// still running (#16054).
qemuPid, err := qemuPid(mc.QEMUHypervisor.QEMUPidPath)
if err != nil {
if stopErr == nil {
return err
}
return fmt.Errorf("%w: %w", stopErr, err)
}
if qemuPid == -1 {
return stopErr
}
if err := sigKill(qemuPid); err != nil {
if stopErr == nil {
return err
}
return fmt.Errorf("%w: %w", stopErr, err)
}
return stopErr
}
// stopLocked stops the machine and expects the caller to hold the machine's lock.
func (q *QEMUStubber) stopLocked(mc *vmconfigs.MachineConfig) error {
// check if the qmp socket is there. if not, qemu instance is gone
if err := fileutils.Exists(mc.QEMUHypervisor.QMPMonitor.Address.GetPath()); errors.Is(err, fs.ErrNotExist) {
// Right now it is NOT an error to stop a stopped machine
logrus.Debugf("QMP monitor socket %v does not exist", mc.QEMUHypervisor.QMPMonitor.Address)
// Fix incorrect starting state in case of crash during start
if mc.Starting {
mc.Starting = false
if err := mc.Write(); err != nil {
return err
}
}
return nil
}
qmpMonitor, err := qmp.NewSocketMonitor(mc.QEMUHypervisor.QMPMonitor.Network, mc.QEMUHypervisor.QMPMonitor.Address.GetPath(), mc.QEMUHypervisor.QMPMonitor.Timeout)
if err != nil {
return err
}
// Simple JSON formation for the QAPI
stopCommand := struct {
Execute string `json:"execute"`
}{
Execute: "system_powerdown",
}
input, err := json.Marshal(stopCommand)
if err != nil {
return err
}
if err := qmpMonitor.Connect(); err != nil {
return err
}
var disconnected bool
defer func() {
if !disconnected {
if err := qmpMonitor.Disconnect(); err != nil {
logrus.Error(err)
}
}
}()
if _, err = qmpMonitor.Run(input); err != nil {
return err
}
// Remove socket
if err := mc.QEMUHypervisor.QMPMonitor.Address.Delete(); err != nil {
return err
}
if err := qmpMonitor.Disconnect(); err != nil {
// FIXME: this error should probably be returned
return nil //nolint: nilerr
}
disconnected = true
if mc.QEMUHypervisor.QEMUPidPath.GetPath() == "" {
// no vm pid file path means it's probably a machine created before we
// started using it, so we revert to the old way of waiting for the
// machine to stop
return q.waitForMachineToStop(mc)
}
vmPid, err := mc.QEMUHypervisor.QEMUPidPath.ReadPIDFrom()
if err != nil {
return err
}
fmt.Println("Waiting for VM to exit...")
for isProcessAlive(vmPid) {
time.Sleep(500 * time.Millisecond)
}
return nil
}
// Remove deletes all the files associated with a machine including the image itself
func (q *QEMUStubber) Remove(mc *vmconfigs.MachineConfig) ([]string, func() error, error) {
qemuRmFiles := []string{
mc.QEMUHypervisor.QEMUPidPath.GetPath(),
mc.QEMUHypervisor.QMPMonitor.Address.GetPath(),
}
return qemuRmFiles, func() error {
var errs []error
if err := mc.QEMUHypervisor.QEMUPidPath.Delete(); err != nil {
errs = append(errs, err)
}
if err := mc.QEMUHypervisor.QMPMonitor.Address.Delete(); err != nil {
errs = append(errs, err)
}
return errorhandling.JoinErrors(errs)
}, nil
}
func (q *QEMUStubber) State(mc *vmconfigs.MachineConfig, bypass bool) (define.Status, error) {
// Check if qmp socket path exists
if err := fileutils.Exists(mc.QEMUHypervisor.QMPMonitor.Address.GetPath()); errors.Is(err, fs.ErrNotExist) {
return define.Stopped, nil
}
if err := mc.Refresh(); err != nil {
return "", err
}
// TODO this has always been a problem, lets fix this
// Check if we can dial it
// if v.Starting && !bypass {
// return define.Starting, nil
// }
monitor, err := qmp.NewSocketMonitor(mc.QEMUHypervisor.QMPMonitor.Network, mc.QEMUHypervisor.QMPMonitor.Address.GetPath(), mc.QEMUHypervisor.QMPMonitor.Timeout)
if err != nil {
// If an improper cleanup was done and the socketmonitor was not deleted,
// it can appear as though the machine state is not stopped. Check for ECONNREFUSED
// almost assures us that the vm is stopped.
if errors.Is(err, syscall.ECONNREFUSED) {
return define.Stopped, nil
}
return "", err
}
if err := monitor.Connect(); err != nil {
// There is a case where if we stop the same vm (from running) two
// consecutive times we can get an econnreset when trying to get the
// state
if errors.Is(err, syscall.ECONNRESET) {
// try again
logrus.Debug("received ECCONNRESET from QEMU monitor; trying again")
secondTry := monitor.Connect()
if errors.Is(secondTry, io.EOF) {
return define.Stopped, nil
}
if secondTry != nil {
logrus.Debugf("second attempt to connect to QEMU monitor failed")
return "", secondTry
}
}
return "", err
}
defer func() {
if err := monitor.Disconnect(); err != nil {
logrus.Error(err)
}
}()
// If there is a monitor, let's see if we can query state
return q.checkStatus(monitor)
}
// executes qemu-image info to get the virtual disk size
// of the diskimage
func getDiskSize(path string) (uint64, error) { //nolint:unused
// Find the qemu executable
cfg, err := config.Default()
if err != nil {
return 0, err
}
qemuPathDir, err := cfg.FindHelperBinary("qemu-img", true)
if err != nil {
return 0, err
}
diskInfo := exec.Command(qemuPathDir, "info", "--output", "json", path)
stdout, err := diskInfo.StdoutPipe()
if err != nil {
return 0, err
}
if err := diskInfo.Start(); err != nil {
return 0, err
}
tmpInfo := struct {
VirtualSize uint64 `json:"virtual-size"`
Filename string `json:"filename"`
ClusterSize int64 `json:"cluster-size"`
Format string `json:"format"`
FormatSpecific struct {
Type string `json:"type"`
Data map[string]string `json:"data"`
}
DirtyFlag bool `json:"dirty-flag"`
}{}
if err := json.NewDecoder(stdout).Decode(&tmpInfo); err != nil {
return 0, err
}
if err := diskInfo.Wait(); err != nil {
return 0, err
}
return tmpInfo.VirtualSize, nil
}