Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/cmd/limactl/network.go
1645 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package main
5
6
import (
7
"encoding/json"
8
"errors"
9
"fmt"
10
"maps"
11
"net"
12
"os"
13
"slices"
14
"strings"
15
"text/tabwriter"
16
17
"github.com/sirupsen/logrus"
18
"github.com/spf13/cobra"
19
20
"github.com/lima-vm/lima/v2/pkg/networks"
21
"github.com/lima-vm/lima/v2/pkg/yqutil"
22
)
23
24
const networkCreateExample = ` Create a network:
25
$ limactl network create foo --gateway 192.168.42.1/24
26
27
Connect VM instances to the newly created network:
28
$ limactl create --network lima:foo --name vm1
29
$ limactl create --network lima:foo --name vm2
30
`
31
32
func newNetworkCommand() *cobra.Command {
33
networkCommand := &cobra.Command{
34
Use: "network",
35
Short: "Lima network management",
36
Example: networkCreateExample,
37
GroupID: advancedCommand,
38
}
39
networkCommand.AddCommand(
40
newNetworkListCommand(),
41
newNetworkCreateCommand(),
42
newNetworkDeleteCommand(),
43
)
44
return networkCommand
45
}
46
47
func newNetworkListCommand() *cobra.Command {
48
cmd := &cobra.Command{
49
Use: "list",
50
Short: "List networks",
51
Aliases: []string{"ls"},
52
Args: WrapArgsError(cobra.ArbitraryArgs),
53
RunE: networkListAction,
54
ValidArgsFunction: networkBashComplete,
55
}
56
flags := cmd.Flags()
57
flags.Bool("json", false, "JSONify output")
58
return cmd
59
}
60
61
func networkListAction(cmd *cobra.Command, args []string) error {
62
flags := cmd.Flags()
63
jsonFormat, err := flags.GetBool("json")
64
if err != nil {
65
return err
66
}
67
68
config, err := networks.LoadConfig()
69
if err != nil {
70
return err
71
}
72
73
allNetworks := slices.Sorted(maps.Keys(config.Networks))
74
75
networks := []string{}
76
if len(args) > 0 {
77
for _, arg := range args {
78
matches := nameMatches(arg, allNetworks)
79
if len(matches) > 0 {
80
networks = append(networks, matches...)
81
} else {
82
logrus.Warnf("No network matching %v found.", arg)
83
}
84
}
85
} else {
86
networks = allNetworks
87
}
88
89
if jsonFormat {
90
w := cmd.OutOrStdout()
91
for _, name := range networks {
92
nw, ok := config.Networks[name]
93
if !ok {
94
logrus.Errorf("network %q does not exist", nw)
95
continue
96
}
97
j, err := json.Marshal(nw)
98
if err != nil {
99
return err
100
}
101
fmt.Fprintln(w, string(j))
102
}
103
return nil
104
}
105
106
w := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0)
107
fmt.Fprintln(w, "NAME\tMODE\tGATEWAY\tINTERFACE")
108
for _, name := range networks {
109
nw, ok := config.Networks[name]
110
if !ok {
111
logrus.Errorf("network %q does not exist", nw)
112
continue
113
}
114
gwStr := "-"
115
if nw.Gateway != nil {
116
gw := net.IPNet{
117
IP: nw.Gateway,
118
Mask: net.IPMask(nw.NetMask),
119
}
120
gwStr = gw.String()
121
}
122
intfStr := "-"
123
if nw.Interface != "" {
124
intfStr = nw.Interface
125
}
126
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, nw.Mode, gwStr, intfStr)
127
}
128
return w.Flush()
129
}
130
131
func newNetworkCreateCommand() *cobra.Command {
132
cmd := &cobra.Command{
133
Use: "create NETWORK",
134
Short: "Create a Lima network",
135
Example: networkCreateExample,
136
Args: WrapArgsError(cobra.ExactArgs(1)),
137
RunE: networkCreateAction,
138
}
139
flags := cmd.Flags()
140
flags.String("mode", networks.ModeUserV2, "mode")
141
_ = cmd.RegisterFlagCompletionFunc("mode", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
142
return networks.Modes, cobra.ShellCompDirectiveNoFileComp
143
})
144
flags.String("gateway", "", "gateway, e.g., \"192.168.42.1/24\"")
145
flags.String("interface", "", "interface for bridged mode")
146
_ = cmd.RegisterFlagCompletionFunc("interface", bashFlagCompleteNetworkInterfaceNames)
147
return cmd
148
}
149
150
func networkCreateAction(cmd *cobra.Command, args []string) error {
151
name := args[0]
152
// LoadConfig ensures existence of networks.yaml
153
config, err := networks.LoadConfig()
154
if err != nil {
155
return err
156
}
157
if _, ok := config.Networks[name]; ok {
158
return fmt.Errorf("network %q already exists", name)
159
}
160
161
flags := cmd.Flags()
162
mode, err := flags.GetString("mode")
163
if err != nil {
164
return err
165
}
166
167
gateway, err := flags.GetString("gateway")
168
if err != nil {
169
return err
170
}
171
172
intf, err := flags.GetString("interface")
173
if err != nil {
174
return err
175
}
176
177
switch mode {
178
case networks.ModeBridged:
179
if gateway != "" {
180
return fmt.Errorf("network mode %q does not support specifying gateway", mode)
181
}
182
if intf == "" {
183
return fmt.Errorf("network mode %q requires specifying interface", mode)
184
}
185
yq := fmt.Sprintf(`.networks.%q = {"mode":%q,"interface":%q}`, name, mode, intf)
186
return networkApplyYQ(yq)
187
default:
188
if gateway == "" {
189
return fmt.Errorf("network mode %q requires specifying gateway", mode)
190
}
191
if intf != "" {
192
return fmt.Errorf("network mode %q does not support specifying interface", mode)
193
}
194
if !strings.Contains(gateway, "/") {
195
gateway += "/24"
196
}
197
gwIP, gwMask, err := net.ParseCIDR(gateway)
198
if err != nil {
199
return fmt.Errorf("failed to parse CIDR %q: %w", gateway, err)
200
}
201
if gwIP.IsUnspecified() || gwIP.IsLoopback() {
202
return fmt.Errorf("invalid IP address: %v", gwIP)
203
}
204
gwMaskStr := "255.255.255.0"
205
if gwMask != nil {
206
gwMaskStr = net.IP(gwMask.Mask).String()
207
}
208
// TODO: check IP range collision
209
210
yq := fmt.Sprintf(`.networks.%q = {"mode":%q,"gateway":%q,"netmask":%q,"interface":%q}`, name, mode, gwIP.String(), gwMaskStr, intf)
211
return networkApplyYQ(yq)
212
}
213
}
214
215
func networkApplyYQ(yq string) error {
216
filePath, err := networks.ConfigFile()
217
if err != nil {
218
return err
219
}
220
yContent, err := os.ReadFile(filePath)
221
if err != nil {
222
return err
223
}
224
yBytes, err := yqutil.EvaluateExpression(yq, yContent)
225
if err != nil {
226
return err
227
}
228
if err := os.WriteFile(filePath, yBytes, 0o644); err != nil {
229
return err
230
}
231
return nil
232
}
233
234
func newNetworkDeleteCommand() *cobra.Command {
235
cmd := &cobra.Command{
236
Use: "delete NETWORK [NETWORK, ...]",
237
Short: "Delete one or more Lima networks",
238
Aliases: []string{"remove", "rm"},
239
Args: WrapArgsError(cobra.MinimumNArgs(1)),
240
RunE: networkDeleteAction,
241
ValidArgsFunction: networkBashComplete,
242
}
243
flags := cmd.Flags()
244
flags.BoolP("force", "f", false, "Force delete (currently always required)")
245
return cmd
246
}
247
248
func networkDeleteAction(cmd *cobra.Command, args []string) error {
249
flags := cmd.Flags()
250
force, err := flags.GetBool("force")
251
if err != nil {
252
return err
253
}
254
if !force {
255
return errors.New("`limactl network delete` currently always requires `--force`")
256
// Because the command currently does not check whether the network being removed is in use
257
}
258
259
var yq string
260
for i, name := range args {
261
yq += fmt.Sprintf("del(.networks.%q)", name)
262
if i < len(args)-1 {
263
yq += " | "
264
}
265
}
266
return networkApplyYQ(yq)
267
}
268
269
func networkBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
270
return bashCompleteNetworkNames(cmd)
271
}
272
273