Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/networks/reconcile/reconcile.go
2655 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package reconcile
5
6
import (
7
"bytes"
8
"context"
9
"fmt"
10
"os"
11
"os/exec"
12
"runtime"
13
"strings"
14
"sync"
15
"time"
16
17
"github.com/sirupsen/logrus"
18
19
"github.com/lima-vm/lima/v2/pkg/limatype"
20
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
21
"github.com/lima-vm/lima/v2/pkg/networks"
22
"github.com/lima-vm/lima/v2/pkg/networks/usernet"
23
"github.com/lima-vm/lima/v2/pkg/osutil"
24
"github.com/lima-vm/lima/v2/pkg/store"
25
)
26
27
func Reconcile(ctx context.Context, newInst string) error {
28
cfg, err := networks.LoadConfig()
29
if err != nil {
30
return err
31
}
32
instances, err := store.Instances()
33
if err != nil {
34
return err
35
}
36
activeNetwork := make(map[string]bool, 3)
37
for _, instName := range instances {
38
instance, err := store.Inspect(ctx, instName)
39
if err != nil {
40
return err
41
}
42
// newInst is about to be started, so its networks should be running
43
if instance.Status != limatype.StatusRunning && instName != newInst {
44
continue
45
}
46
for _, nw := range instance.Networks {
47
if nw.Lima == "" {
48
continue
49
}
50
if _, ok := cfg.Networks[nw.Lima]; !ok {
51
logrus.Errorf("network %q (used by instance %q) is missing from networks.yaml", nw.Lima, instName)
52
continue
53
}
54
activeNetwork[nw.Lima] = true
55
}
56
}
57
for name := range cfg.Networks {
58
var err error
59
if activeNetwork[name] {
60
err = startNetwork(ctx, &cfg, name)
61
} else {
62
err = stopNetwork(ctx, &cfg, name)
63
}
64
if err != nil {
65
return err
66
}
67
}
68
return nil
69
}
70
71
func sudo(ctx context.Context, user, group, command string) error {
72
args := []string{"--user", user, "--group", group, "--non-interactive"}
73
args = append(args, strings.Split(command, " ")...)
74
var stdout, stderr bytes.Buffer
75
cmd := exec.CommandContext(ctx, "sudo", args...)
76
cmd.Stdout = &stdout
77
cmd.Stderr = &stderr
78
logrus.Debugf("Running: %v", cmd.Args)
79
if err := cmd.Run(); err != nil {
80
return fmt.Errorf("failed to run %v: stdout=%q, stderr=%q: %w",
81
cmd.Args, stdout.String(), stderr.String(), err)
82
}
83
return nil
84
}
85
86
func makeVarRun(ctx context.Context, cfg *networks.Config) error {
87
err := sudo(ctx, "root", "wheel", cfg.MkdirCmd())
88
if err != nil {
89
return err
90
}
91
92
// Check that VarRun is daemon-group writable. If we don't report it here, the error would only be visible
93
// in the vde_switch daemon log. This has not been checked by networks.Validate() because only the VarRun
94
// directory itself needs to be daemon-group writable, any parents just need to be daemon-group executable.
95
fi, err := os.Stat(cfg.Paths.VarRun)
96
if err != nil {
97
return err
98
}
99
stat, ok := osutil.SysStat(fi)
100
if !ok {
101
// should never happen
102
return fmt.Errorf("could not retrieve stat buffer for %q", cfg.Paths.VarRun)
103
}
104
daemon, err := osutil.LookupUser("daemon")
105
if err != nil {
106
return err
107
}
108
if fi.Mode()&0o20 == 0 || stat.Gid != daemon.Gid {
109
return fmt.Errorf("%q doesn't seem to be writable by the daemon (gid:%d) group",
110
cfg.Paths.VarRun, daemon.Gid)
111
}
112
return nil
113
}
114
115
func startDaemon(ctx context.Context, cfg *networks.Config, name, daemon string) error {
116
if err := makeVarRun(ctx, cfg); err != nil {
117
return err
118
}
119
networksDir, err := dirnames.LimaNetworksDir()
120
if err != nil {
121
return err
122
}
123
if err := os.MkdirAll(networksDir, 0o755); err != nil {
124
return err
125
}
126
user, err := cfg.User(daemon)
127
if err != nil {
128
return err
129
}
130
131
args := []string{"--user", user.User, "--group", user.Group, "--non-interactive"}
132
args = append(args, strings.Split(cfg.StartCmd(name, daemon), " ")...)
133
cmd := exec.CommandContext(ctx, "sudo", args...)
134
// set directory to a path the daemon user has read access to because vde_switch calls getcwd() which
135
// can fail when called from directories like ~/Downloads, which has 700 permissions
136
cmd.Dir = cfg.Paths.VarRun
137
138
stdoutPath := cfg.LogFile(name, daemon, "stdout")
139
stderrPath := cfg.LogFile(name, daemon, "stderr")
140
if err := os.RemoveAll(stdoutPath); err != nil {
141
return err
142
}
143
if err := os.RemoveAll(stderrPath); err != nil {
144
return err
145
}
146
147
cmd.Stdout, err = os.Create(stdoutPath)
148
if err != nil {
149
return err
150
}
151
cmd.Stderr, err = os.Create(stderrPath)
152
if err != nil {
153
return err
154
}
155
156
logrus.Debugf("Starting %q daemon for %q network: %v", daemon, name, cmd.Args)
157
if err := cmd.Start(); err != nil {
158
return fmt.Errorf("failed to run %v: %w (Hint: check %q, %q)", cmd.Args, err, stdoutPath, stderrPath)
159
}
160
return nil
161
}
162
163
var validation struct {
164
sync.Once
165
err error
166
}
167
168
func validateConfig(ctx context.Context, cfg *networks.Config) error {
169
validation.Do(func() {
170
// make sure all cfg.Paths.* are secure
171
validation.err = cfg.Validate()
172
if validation.err == nil {
173
validation.err = cfg.VerifySudoAccess(ctx, cfg.Paths.Sudoers)
174
}
175
})
176
return validation.err
177
}
178
179
func startNetwork(ctx context.Context, cfg *networks.Config, name string) error {
180
logrus.Debugf("Make sure %q network is running", name)
181
182
// Handle usernet first without sudo requirements
183
isUsernet, err := cfg.Usernet(name)
184
if err != nil {
185
return err
186
}
187
if isUsernet {
188
if err := usernet.Start(ctx, name); err != nil {
189
return fmt.Errorf("failed to start usernet %q: %w", name, err)
190
}
191
return nil
192
}
193
194
if runtime.GOOS != "darwin" {
195
return nil
196
}
197
198
if err := validateConfig(ctx, cfg); err != nil {
199
return err
200
}
201
var daemons []string
202
ok, err := cfg.IsDaemonInstalled(networks.SocketVMNet)
203
if err != nil {
204
return err
205
}
206
if ok {
207
daemons = append(daemons, networks.SocketVMNet)
208
} else {
209
return fmt.Errorf("daemon %q needs to be installed", networks.SocketVMNet)
210
}
211
for _, daemon := range daemons {
212
pid, _ := store.ReadPIDFile(cfg.PIDFile(name, daemon))
213
if pid == 0 {
214
logrus.Infof("Starting %s daemon for %q network", daemon, name)
215
if err := startDaemon(ctx, cfg, name, daemon); err != nil {
216
return err
217
}
218
}
219
}
220
return nil
221
}
222
223
func stopNetwork(ctx context.Context, cfg *networks.Config, name string) error {
224
logrus.Debugf("Make sure %q network is stopped", name)
225
// Handle usernet first without sudo requirements
226
isUsernet, err := cfg.Usernet(name)
227
if err != nil {
228
return err
229
}
230
if isUsernet {
231
if err := usernet.Stop(ctx, name); err != nil {
232
return fmt.Errorf("failed to stop usernet %q: %w", name, err)
233
}
234
return nil
235
}
236
237
if runtime.GOOS != "darwin" {
238
return nil
239
}
240
241
// Don't call validateConfig() until we actually need to stop a daemon because
242
// stopNetwork() may be called even when the daemons are not installed.
243
for _, daemon := range []string{networks.SocketVMNet} {
244
if ok, _ := cfg.IsDaemonInstalled(daemon); !ok {
245
continue
246
}
247
pid, _ := store.ReadPIDFile(cfg.PIDFile(name, daemon))
248
if pid != 0 {
249
logrus.Infof("Stopping %s daemon for %q network", daemon, name)
250
if err := validateConfig(ctx, cfg); err != nil {
251
return err
252
}
253
user, err := cfg.User(daemon)
254
if err != nil {
255
return err
256
}
257
err = sudo(ctx, user.User, user.Group, cfg.StopCmd(name, daemon))
258
if err != nil {
259
return err
260
}
261
}
262
// wait for daemons to terminate (up to 5s) before stopping, otherwise the sockets may not get deleted which
263
// will cause subsequent start commands to fail.
264
startWaiting := time.Now()
265
for {
266
if pid, _ := store.ReadPIDFile(cfg.PIDFile(name, daemon)); pid == 0 {
267
break
268
}
269
if time.Since(startWaiting) > 5*time.Second {
270
logrus.Infof("%q daemon for %q network still running after 5 seconds", daemon, name)
271
break
272
}
273
time.Sleep(500 * time.Millisecond)
274
}
275
}
276
return nil
277
}
278
279