Skip to content

Commit 5301add

Browse files
committed
Support host block-device attachment on macOS VZ
Signed-off-by: Nick Sweeting <git@sweeting.me>
1 parent 7e52923 commit 5301add

46 files changed

Lines changed: 1317 additions & 115 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,7 @@ jobs:
591591
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
592592
with:
593593
persist-credentials: false
594+
submodules: true
594595
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
595596
with:
596597
go-version: stable
@@ -609,6 +610,8 @@ jobs:
609610
run: brew uninstall --ignore-dependencies --force qemu
610611
- name: Test
611612
run: ./hack/test-templates.sh templates/${{ matrix.template }}
613+
- name: Run BATS VZ block-device tests
614+
run: ./hack/bats/lib/bats-core/bin/bats --timing ./hack/bats/extras/vz-block-device.bats
612615
- if: failure()
613616
uses: ./.github/actions/upload_failure_logs_if_exists
614617
with:

cmd/limactl/editflags/editflags.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ func RegisterEdit(cmd *cobra.Command, commentPrefix string) {
6767
flags.Bool("nested-virt", false, commentPrefix+"Enable nested virtualization")
6868

6969
flags.Bool("rosetta", false, commentPrefix+"Enable Rosetta (for vz instances)")
70+
flags.StringSlice("block-device", nil, commentPrefix+"Host block devices to attach on supported backends, e.g. /dev/disk4")
7071

7172
flags.StringArray("set", []string{}, commentPrefix+"Modify the template inplace, using yq syntax. Can be passed multiple times.")
7273

@@ -332,6 +333,25 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) {
332333
false,
333334
false,
334335
},
336+
{
337+
"block-device",
338+
func(_ *flag.Flag) ([]string, error) {
339+
paths, err := flags.GetStringSlice("block-device")
340+
if err != nil {
341+
return nil, err
342+
}
343+
devices := make([]string, len(paths))
344+
for i, path := range paths {
345+
if path == "" {
346+
return nil, errors.New("block device path must not be empty")
347+
}
348+
devices[i] = strconv.Quote(path)
349+
}
350+
return []string{fmt.Sprintf(".blockDevices += [%s] | .blockDevices |= unique", strings.Join(devices, ","))}, nil
351+
},
352+
false,
353+
false,
354+
},
335355
{"set", func(v *flag.Flag) ([]string, error) {
336356
return v.Value.(flag.SliceValue).GetSlice(), nil
337357
}, false, false},

cmd/limactl/editflags/editflags_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,12 @@ func TestYQExpressions(t *testing.T) {
225225
newInstance: false,
226226
expected: []string{`.nestedVirtualization = true`},
227227
},
228+
{
229+
name: "block-device",
230+
args: []string{"--block-device", "/dev/disk4", "--block-device", "/dev/rdisk5"},
231+
newInstance: false,
232+
expected: []string{`.blockDevices += ["/dev/disk4","/dev/rdisk5"] | .blockDevices |= unique`},
233+
},
228234
{
229235
name: "invalid network",
230236
args: []string{"--network", "invalid"},

cmd/limactl/main.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const (
3232
)
3333

3434
func main() {
35-
if os.Geteuid() == 0 && (len(os.Args) < 2 || os.Args[1] != "generate-doc") {
35+
if os.Geteuid() == 0 && (len(os.Args) < 2 || (os.Args[1] != "generate-doc" && !isPrivilegedHelperCommand(os.Args[1]))) {
3636
fmt.Fprint(os.Stderr, "limactl: must not run as the root user\n")
3737
os.Exit(1)
3838
}
@@ -136,6 +136,9 @@ func newApp() *cobra.Command {
136136
// allow commands that are used for packaging to run under rosetta to allow cross-architecture builds
137137
return errors.New("limactl is running under rosetta, please reinstall lima with native arch")
138138
}
139+
if isPrivilegedHelperCommand(cmd.Name()) {
140+
return nil
141+
}
139142

140143
// Make sure either $HOME or $LIMA_HOME is defined, so we don't need
141144
// to check for errors later
@@ -211,6 +214,7 @@ func newApp() *cobra.Command {
211214
newRenameCommand(),
212215
newWatchCommand(),
213216
)
217+
registerHiddenCommands(rootCmd)
214218
addPluginCommands(rootCmd)
215219

216220
return rootCmd
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//go:build darwin
2+
3+
// SPDX-FileCopyrightText: Copyright The Lima Authors
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
package main
7+
8+
import (
9+
"github.com/spf13/cobra"
10+
11+
"github.com/lima-vm/lima/v2/pkg/blockdevice"
12+
)
13+
14+
func isPrivilegedHelperCommand(name string) bool {
15+
return name == blockdevice.SudoOpenBlockDeviceCommand
16+
}
17+
18+
func registerHiddenCommands(rootCmd *cobra.Command) {
19+
rootCmd.AddCommand(&cobra.Command{
20+
Use: blockdevice.SudoOpenBlockDeviceCommand,
21+
Short: "Open a host block device via privileged helper",
22+
Args: WrapArgsError(cobra.NoArgs),
23+
Hidden: true,
24+
RunE: func(cmd *cobra.Command, _ []string) error {
25+
return blockdevice.ServeSudoOpenBlockDevice(cmd.InOrStdin())
26+
},
27+
})
28+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//go:build !darwin
2+
3+
// SPDX-FileCopyrightText: Copyright The Lima Authors
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
package main
7+
8+
import "github.com/spf13/cobra"
9+
10+
func isPrivilegedHelperCommand(string) bool {
11+
return false
12+
}
13+
14+
func registerHiddenCommands(_ *cobra.Command) {}

cmd/limactl/sudoers.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ To validate the existing /etc/sudoers.d/lima file:
2727
$ limactl sudoers --check /etc/sudoers.d/lima
2828
`,
2929
Short: "Generate the content of the /etc/sudoers.d/lima file",
30-
Long: fmt.Sprintf(`Generate the content of the /etc/sudoers.d/lima file for enabling vmnet.framework support (socket_vmnet) on macOS.
30+
Long: fmt.Sprintf(`Generate the content of the /etc/sudoers.d/lima file for macOS host helpers that require privilege escalation.
31+
This includes vmnet.framework support (socket_vmnet) and host block-device attachment with --block-device on supported backends.
3132
The content is written to stdout, NOT to the file.
3233
This command must not run as the root user.
3334
See %s for the usage.`, socketVMNetURL),

cmd/limactl/sudoers_darwin.go

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import (
88
"errors"
99
"fmt"
1010
"io"
11+
"os"
1112

12-
"github.com/sirupsen/logrus"
1313
"github.com/spf13/cobra"
1414

15+
"github.com/lima-vm/lima/v2/pkg/blockdevice"
1516
"github.com/lima-vm/lima/v2/pkg/networks"
17+
"github.com/lima-vm/lima/v2/pkg/sudoers"
1618
)
1719

1820
func sudoersAction(cmd *cobra.Command, args []string) error {
@@ -21,11 +23,6 @@ func sudoersAction(cmd *cobra.Command, args []string) error {
2123
if err != nil {
2224
return err
2325
}
24-
// Make sure the current network configuration is secure
25-
if err := nwCfg.Validate(); err != nil {
26-
logrus.Infof("Please check %s for more information.", socketVMNetURL)
27-
return err
28-
}
2926
check, err := cmd.Flags().GetBool("check")
3027
if err != nil {
3128
return err
@@ -41,11 +38,16 @@ func sudoersAction(cmd *cobra.Command, args []string) error {
4138
default:
4239
return fmt.Errorf("unexpected arguments %v", args)
4340
}
44-
sudoers, err := networks.Sudoers()
41+
networkSudoers, err := nwCfg.Sudoers()
42+
if err != nil {
43+
return err
44+
}
45+
blockDeviceSudoers, err := blockdevice.Sudoers(nwCfg.Group)
4546
if err != nil {
4647
return err
4748
}
48-
fmt.Fprint(cmd.OutOrStdout(), sudoers)
49+
content := sudoers.AssembleSudoersFragments(networkSudoers, blockDeviceSudoers)
50+
fmt.Fprint(cmd.OutOrStdout(), content)
4951
return nil
5052
}
5153

@@ -63,9 +65,38 @@ func verifySudoAccess(ctx context.Context, nwCfg networks.Config, args []string,
6365
default:
6466
return errors.New("can check only a single sudoers file")
6567
}
66-
if err := nwCfg.VerifySudoAccess(ctx, file); err != nil {
68+
if err := verifySudoersFile(ctx, nwCfg, file); err != nil {
6769
return err
6870
}
6971
fmt.Fprintf(stdout, "%q is up-to-date (or sudo doesn't require a password)\n", file)
7072
return nil
7173
}
74+
75+
func verifySudoersFile(ctx context.Context, nwCfg networks.Config, file string) error {
76+
hint := fmt.Sprintf("run `%s sudoers >etc_sudoers.d_lima && sudo install -o root etc_sudoers.d_lima %q`)",
77+
os.Args[0], file)
78+
b, err := os.ReadFile(file)
79+
if err != nil {
80+
if errors.Is(err, os.ErrNotExist) {
81+
if err := nwCfg.VerifySudoAccess(ctx, ""); err == nil {
82+
if err := sudoers.Run(ctx, "root", "wheel", nil, nil, nil, "", "true"); err == nil {
83+
return nil
84+
}
85+
}
86+
}
87+
return fmt.Errorf("can't read %q: %w: (Hint: %s)", file, err, hint)
88+
}
89+
networkSudoers, err := nwCfg.Sudoers()
90+
if err != nil {
91+
return err
92+
}
93+
blockDeviceSudoers, err := blockdevice.Sudoers(nwCfg.Group)
94+
if err != nil {
95+
return err
96+
}
97+
content := sudoers.AssembleSudoersFragments(networkSudoers, blockDeviceSudoers)
98+
if string(b) != content {
99+
return fmt.Errorf("sudoers file %q is out of sync and must be regenerated (Hint: %s)", file, hint)
100+
}
101+
return nil
102+
}

0 commit comments

Comments
 (0)