未验证 提交 0de446dc 编写于 作者: M Medya Ghazizadeh 提交者: GitHub

Merge pull request #10792 from prezha/kvm-network

kvm2 driver: add dedicated network & static ip
...@@ -161,7 +161,7 @@ func initMinikubeFlags() { ...@@ -161,7 +161,7 @@ func initMinikubeFlags() {
startCmd.Flags().Bool(preload, true, "If set, download tarball of preloaded images if available to improve start time. Defaults to true.") startCmd.Flags().Bool(preload, true, "If set, download tarball of preloaded images if available to improve start time. Defaults to true.")
startCmd.Flags().Bool(deleteOnFailure, false, "If set, delete the current cluster if start fails and try again. Defaults to false.") startCmd.Flags().Bool(deleteOnFailure, false, "If set, delete the current cluster if start fails and try again. Defaults to false.")
startCmd.Flags().Bool(forceSystemd, false, "If set, force the container runtime to use sytemd as cgroup manager. Defaults to false.") startCmd.Flags().Bool(forceSystemd, false, "If set, force the container runtime to use sytemd as cgroup manager. Defaults to false.")
startCmd.Flags().StringP(network, "", "", "network to run minikube with. Only available with the docker/podman drivers. If left empty, minikube will create a new network.") startCmd.Flags().StringP(network, "", "", "network to run minikube with. Now it is used by docker/podman and KVM drivers. If left empty, minikube will create a new network.")
startCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Format to print stdout in. Options include: [text,json]") startCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Format to print stdout in. Options include: [text,json]")
startCmd.Flags().StringP(trace, "", "", "Send trace events. Options include: [gcp]") startCmd.Flags().StringP(trace, "", "", "Send trace events. Options include: [gcp]")
} }
...@@ -191,7 +191,7 @@ func initDriverFlags() { ...@@ -191,7 +191,7 @@ func initDriverFlags() {
startCmd.Flags().Bool("vm", false, "Filter to use only VM Drivers") startCmd.Flags().Bool("vm", false, "Filter to use only VM Drivers")
// kvm2 // kvm2
startCmd.Flags().String(kvmNetwork, "default", "The KVM network name. (kvm2 driver only)") startCmd.Flags().String(kvmNetwork, "default", "The KVM default network name. (kvm2 driver only)")
startCmd.Flags().String(kvmQemuURI, "qemu:///system", "The KVM QEMU connection URI. (kvm2 driver only)") startCmd.Flags().String(kvmQemuURI, "qemu:///system", "The KVM QEMU connection URI. (kvm2 driver only)")
startCmd.Flags().Bool(kvmGPU, false, "Enable experimental NVIDIA GPU support in minikube") startCmd.Flags().Bool(kvmGPU, false, "Enable experimental NVIDIA GPU support in minikube")
startCmd.Flags().Bool(kvmHidden, false, "Hide the hypervisor signature from the guest in minikube (kvm2 driver only)") startCmd.Flags().Bool(kvmHidden, false, "Hide the hypervisor signature from the guest in minikube (kvm2 driver only)")
...@@ -311,8 +311,8 @@ func generateClusterConfig(cmd *cobra.Command, existing *config.ClusterConfig, k ...@@ -311,8 +311,8 @@ func generateClusterConfig(cmd *cobra.Command, existing *config.ClusterConfig, k
out.WarningT("With --network-plugin=cni, you will need to provide your own CNI. See --cni flag as a user-friendly alternative") out.WarningT("With --network-plugin=cni, you will need to provide your own CNI. See --cni flag as a user-friendly alternative")
} }
if !driver.IsKIC(drvName) && viper.GetString(network) != "" { if !(driver.IsKIC(drvName) || driver.IsKVM(drvName)) && viper.GetString(network) != "" {
out.WarningT("--network flag is only valid with the docker/podman drivers, it will be ignored") out.WarningT("--network flag is only valid with the docker/podman and KVM drivers, it will be ignored")
} }
checkNumaCount(k8sVersion) checkNumaCount(k8sVersion)
......
...@@ -20,9 +20,7 @@ package kvm ...@@ -20,9 +20,7 @@ package kvm
import ( import (
"bytes" "bytes"
"crypto/rand"
"fmt" "fmt"
"net"
"text/template" "text/template"
libvirt "github.com/libvirt/libvirt-go" libvirt "github.com/libvirt/libvirt-go"
...@@ -68,12 +66,10 @@ const domainTmpl = ` ...@@ -68,12 +66,10 @@ const domainTmpl = `
</disk> </disk>
<interface type='network'> <interface type='network'>
<source network='{{.Network}}'/> <source network='{{.Network}}'/>
<mac address='{{.MAC}}'/>
<model type='virtio'/> <model type='virtio'/>
</interface> </interface>
<interface type='network'> <interface type='network'>
<source network='{{.PrivateNetwork}}'/> <source network='{{.PrivateNetwork}}'/>
<mac address='{{.PrivateMAC}}'/>
<model type='virtio'/> <model type='virtio'/>
</interface> </interface>
<serial type='pty'> <serial type='pty'>
...@@ -92,25 +88,6 @@ const domainTmpl = ` ...@@ -92,25 +88,6 @@ const domainTmpl = `
</domain> </domain>
` `
func randomMAC() (net.HardwareAddr, error) {
buf := make([]byte, 6)
_, err := rand.Read(buf)
if err != nil {
return nil, err
}
// We unset the first and second least significant bits (LSB) of the MAC
//
// The LSB of the first octet
// 0 for unicast
// 1 for multicast
//
// The second LSB of the first octet
// 0 for universally administered addresses
// 1 for locally administered addresses
buf[0] &= 0xfc
return buf, nil
}
func (d *Driver) getDomain() (*libvirt.Domain, *libvirt.Connect, error) { func (d *Driver) getDomain() (*libvirt.Domain, *libvirt.Connect, error) {
conn, err := getConnection(d.ConnectionURI) conn, err := getConnection(d.ConnectionURI)
if err != nil { if err != nil {
...@@ -146,22 +123,6 @@ func closeDomain(dom *libvirt.Domain, conn *libvirt.Connect) error { ...@@ -146,22 +123,6 @@ func closeDomain(dom *libvirt.Domain, conn *libvirt.Connect) error {
} }
func (d *Driver) createDomain() (*libvirt.Domain, error) { func (d *Driver) createDomain() (*libvirt.Domain, error) {
// create random MAC addresses first for our NICs
if d.MAC == "" {
mac, err := randomMAC()
if err != nil {
return nil, errors.Wrap(err, "generating mac address")
}
d.MAC = mac.String()
}
if d.PrivateMAC == "" {
mac, err := randomMAC()
if err != nil {
return nil, errors.Wrap(err, "generating mac address")
}
d.PrivateMAC = mac.String()
}
// create the XML for the domain using our domainTmpl template // create the XML for the domain using our domainTmpl template
tmpl := template.Must(template.New("domain").Parse(domainTmpl)) tmpl := template.Must(template.New("domain").Parse(domainTmpl))
var domainXML bytes.Buffer var domainXML bytes.Buffer
...@@ -180,5 +141,17 @@ func (d *Driver) createDomain() (*libvirt.Domain, error) { ...@@ -180,5 +141,17 @@ func (d *Driver) createDomain() (*libvirt.Domain, error) {
return nil, errors.Wrapf(err, "error defining domain xml: %s", domainXML.String()) return nil, errors.Wrapf(err, "error defining domain xml: %s", domainXML.String())
} }
// save MAC address
dmac, err := macFromXML(conn, d.MachineName, d.Network)
if err != nil {
return nil, fmt.Errorf("failed saving MAC address: %w", err)
}
d.MAC = dmac
pmac, err := macFromXML(conn, d.MachineName, d.PrivateNetwork)
if err != nil {
return nil, fmt.Errorf("failed saving MAC address: %w", err)
}
d.PrivateMAC = pmac
return dom, nil return dom, nil
} }
...@@ -31,6 +31,7 @@ import ( ...@@ -31,6 +31,7 @@ import (
libvirt "github.com/libvirt/libvirt-go" libvirt "github.com/libvirt/libvirt-go"
"github.com/pkg/errors" "github.com/pkg/errors"
pkgdrivers "k8s.io/minikube/pkg/drivers" pkgdrivers "k8s.io/minikube/pkg/drivers"
"k8s.io/minikube/pkg/util/retry"
) )
// Driver is the machine driver for KVM // Driver is the machine driver for KVM
...@@ -209,12 +210,14 @@ func (d *Driver) GetIP() (string, error) { ...@@ -209,12 +210,14 @@ func (d *Driver) GetIP() (string, error) {
if s != state.Running { if s != state.Running {
return "", errors.New("host is not running") return "", errors.New("host is not running")
} }
ip, err := d.lookupIP()
conn, err := getConnection(d.ConnectionURI)
if err != nil { if err != nil {
return "", errors.Wrap(err, "getting IP") return "", errors.Wrap(err, "getting libvirt connection")
} }
defer conn.Close()
return ip, nil return ipFromXML(conn, d.MachineName, d.PrivateNetwork)
} }
// GetSSHHostname returns hostname for use with ssh // GetSSHHostname returns hostname for use with ssh
...@@ -272,32 +275,43 @@ func (d *Driver) Start() (err error) { ...@@ -272,32 +275,43 @@ func (d *Driver) Start() (err error) {
} }
log.Info("Waiting to get IP...") log.Info("Waiting to get IP...")
for i := 0; i <= 40; i++ { if err := d.waitForStaticIP(conn); err != nil {
ip, err := d.GetIP() return errors.Wrap(err, "IP not available after waiting")
}
log.Info("Waiting for SSH to be available...")
if err := drivers.WaitForSSH(d); err != nil {
return errors.Wrap(err, "SSH not available after waiting")
}
return nil
}
// waitForStaticIP waits for IP address of domain that has been created & starting and then makes that IP static.
func (d *Driver) waitForStaticIP(conn *libvirt.Connect) error {
query := func() error {
sip, err := ipFromAPI(conn, d.MachineName, d.PrivateNetwork)
if err != nil { if err != nil {
return errors.Wrap(err, "getting ip during machine start") return fmt.Errorf("failed getting IP during machine start, will retry: %w", err)
} }
if ip == "" { if sip == "" {
log.Debugf("Waiting for machine to come up %d/%d", i, 40) return fmt.Errorf("waiting for machine to come up")
time.Sleep(3 * time.Second)
continue
} }
if ip != "" { log.Infof("Found IP for machine: %s", sip)
log.Infof("Found IP for machine: %s", ip) d.IPAddress = sip
d.IPAddress = ip
break
}
}
if d.IPAddress == "" { return nil
return errors.New("machine didn't return an IP after 120 seconds") }
if err := retry.Local(query, 1*time.Minute); err != nil {
return fmt.Errorf("machine %s didn't return IP after 1 minute", d.MachineName)
} }
log.Info("Waiting for SSH to be available...") log.Info("Reserving static IP address...")
if err := drivers.WaitForSSH(d); err != nil { if err := addStaticIP(conn, d.PrivateNetwork, d.MachineName, d.PrivateMAC, d.IPAddress); err != nil {
d.IPAddress = "" log.Warnf("Failed reserving static IP %s for host %s, will continue anyway: %v", d.IPAddress, d.MachineName, err)
return errors.Wrap(err, "SSH not available after waiting") } else {
log.Infof("Reserved static IP address: %s", d.IPAddress)
} }
return nil return nil
...@@ -385,7 +399,6 @@ func ensureDirPermissions(store string) error { ...@@ -385,7 +399,6 @@ func ensureDirPermissions(store string) error {
// Stop a host gracefully // Stop a host gracefully
func (d *Driver) Stop() (err error) { func (d *Driver) Stop() (err error) {
d.IPAddress = ""
s, err := d.GetState() s, err := d.GetState()
if err != nil { if err != nil {
return errors.Wrap(err, "getting state of VM") return errors.Wrap(err, "getting state of VM")
...@@ -458,6 +471,13 @@ func (d *Driver) Remove() error { ...@@ -458,6 +471,13 @@ func (d *Driver) Remove() error {
return errors.Wrap(err, "undefine domain") return errors.Wrap(err, "undefine domain")
} }
log.Info("Removing static IP address...")
if err := delStaticIP(conn, d.PrivateNetwork, "", "", d.IPAddress); err != nil {
log.Warnf("failed removing static IP %s for host %s, will continue anyway: %v", d.IPAddress, d.MachineName, err)
} else {
log.Info("Removed static IP address")
}
return nil return nil
} }
......
...@@ -20,11 +20,8 @@ package kvm ...@@ -20,11 +20,8 @@ package kvm
import ( import (
"bytes" "bytes"
"encoding/json"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io/ioutil"
"strings"
"text/template" "text/template"
"time" "time"
...@@ -56,6 +53,27 @@ type kvmNetwork struct { ...@@ -56,6 +53,27 @@ type kvmNetwork struct {
network.Parameters network.Parameters
} }
type kvmIface struct {
Type string `xml:"type,attr"`
Mac struct {
Address string `xml:"address,attr"`
} `xml:"mac"`
Source struct {
Network string `xml:"network,attr"`
Portid string `xml:"portid,attr"`
Bridge string `xml:"bridge,attr"`
} `xml:"source"`
Target struct {
Dev string `xml:"dev,attr"`
} `xml:"target"`
Model struct {
Type string `xml:"type,attr"`
} `xml:"model"`
Alias struct {
Name string `xml:"name,attr"`
} `xml:"alias"`
}
// firstSubnetAddr is starting subnet to try for new KVM cluster, // firstSubnetAddr is starting subnet to try for new KVM cluster,
// avoiding possible conflict with other local networks by further incrementing it up to 20 times by 10. // avoiding possible conflict with other local networks by further incrementing it up to 20 times by 10.
const firstSubnetAddr = "192.168.39.0" const firstSubnetAddr = "192.168.39.0"
...@@ -335,94 +353,195 @@ func (d *Driver) checkDomains(conn *libvirt.Connect) error { ...@@ -335,94 +353,195 @@ func (d *Driver) checkDomains(conn *libvirt.Connect) error {
return nil return nil
} }
func (d *Driver) lookupIP() (string, error) { // Static IP management
conn, err := getConnection(d.ConnectionURI) // "Update ... existing network definition, with the changes ... taking effect immediately, without needing to destroy and re-start the network."
// ref: https://libvirt.org/manpages/virsh.html#net-update
// ref: https://libvirt.org/html/libvirt-libvirt-network.html#virNetworkUpdate
// ref: https://wiki.libvirt.org/page/Networking#Applying_modifications_to_the_network
// ref: https://libvirt.org/formatnetwork.html#elementsAddress
// ref: https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainInterfaceAddresses
// ref: https://libvirt.org/manpages/virsh.html#domifaddr
// addStaticIP appends new host's name, MAC and static IP address record to list of network DHCP leases.
// It will return nil if host record already exists.
func addStaticIP(conn *libvirt.Connect, network, hostname, mac, ip string) error {
l, err := dhcpLease(conn, network, hostname, mac, ip)
if err != nil { if err != nil {
return "", errors.Wrap(err, "getting connection and domain") return fmt.Errorf("failed looking up network %s for host DHCP lease {name: %q, mac: %q, ip: %q}: %w", network, hostname, mac, ip, err)
}
if l != nil {
log.Debugf("skip adding static IP to network %s - found existing host DHCP lease matching {name: %q, mac: %q, ip: %q}", network, hostname, mac, ip)
return nil
} }
defer conn.Close()
libVersion, err := conn.GetLibVersion() net, err := conn.LookupNetworkByName(network)
if err != nil { if err != nil {
return "", errors.Wrap(err, "getting libversion") return fmt.Errorf("failed looking up network %s: %w", network, err)
}
defer func() { _ = net.Free() }()
return net.Update(
libvirt.NETWORK_UPDATE_COMMAND_ADD_LAST,
libvirt.NETWORK_SECTION_IP_DHCP_HOST,
-1,
fmt.Sprintf("<host mac=%q name=%q ip=%q/>", mac, hostname, ip),
libvirt.NETWORK_UPDATE_AFFECT_LIVE+libvirt.NETWORK_UPDATE_AFFECT_CONFIG)
}
// delStaticIP deletes static IP address record that matches given combination of host's name, MAC and IP from list of network DHCP leases.
// It will return nil if record doesn't exist.
func delStaticIP(conn *libvirt.Connect, network, hostname, mac, ip string) error {
l, err := dhcpLease(conn, network, hostname, mac, ip)
if err != nil {
return fmt.Errorf("failed looking up network %s for host DHCP lease {name: %q, mac: %q, ip: %q}: %w", network, hostname, mac, ip, err)
}
if l == nil {
log.Debugf("skip deleting static IP from network %s - couldn't find host DHCP lease matching {name: %q, mac: %q, ip: %q}", network, hostname, mac, ip)
return nil
} }
// Earlier versions of libvirt use a lease file instead of a status file net, err := conn.LookupNetworkByName(network)
if libVersion < 1002006 { if err != nil {
return d.lookupIPFromLeasesFile() return fmt.Errorf("failed looking up network %s: %w", network, err)
} }
defer func() { _ = net.Free() }()
// TODO: for everything > 1002006, there is direct support in the libvirt-go for handling this return net.Update(
return d.lookupIPFromStatusFile(conn) libvirt.NETWORK_UPDATE_COMMAND_DELETE,
libvirt.NETWORK_SECTION_IP_DHCP_HOST,
-1,
fmt.Sprintf("<host mac=%q name=%q ip=%q/>", l.Mac, l.Hostname, l.IPaddr),
libvirt.NETWORK_UPDATE_AFFECT_LIVE+libvirt.NETWORK_UPDATE_AFFECT_CONFIG)
} }
func (d *Driver) lookupIPFromStatusFile(conn *libvirt.Connect) (string, error) { // dhcpLease returns network DHCP lease that matches given combination of host's name, MAC and IP.
network, err := conn.LookupNetworkByName(d.PrivateNetwork) func dhcpLease(conn *libvirt.Connect, network, hostname, mac, ip string) (lease *libvirt.NetworkDHCPLease, err error) {
if hostname == "" && mac == "" && ip == "" {
return nil, nil
}
net, err := conn.LookupNetworkByName(network)
if err != nil { if err != nil {
return "", errors.Wrap(err, "looking up network by name") return nil, fmt.Errorf("failed looking up network %s: %w", network, err)
} }
defer func() { _ = network.Free() }() defer func() { _ = net.Free() }()
bridge, err := network.GetBridgeName() leases, err := net.GetDHCPLeases()
if err != nil { if err != nil {
log.Warnf("Failed to get network bridge: %v", err) return nil, fmt.Errorf("failed getting host DHCP leases: %w", err)
return "", err
} }
statusFile := fmt.Sprintf("/var/lib/libvirt/dnsmasq/%s.status", bridge)
statuses, err := ioutil.ReadFile(statusFile) for _, l := range leases {
if (hostname == "" || hostname == l.Hostname) && (mac == "" || mac == l.Mac) && (ip == "" || ip == l.IPaddr) {
log.Debugf("found host DHCP lease matching {name: %q, mac: %q, ip: %q} in network %s: %+v", hostname, mac, ip, network, l)
return &l, nil
}
}
log.Debugf("unable to find host DHCP lease matching {name: %q, mac: %q, ip: %q} in network %s", hostname, mac, ip, network)
return nil, nil
}
// ipFromAPI returns current primary IP address of domain interface in network.
func ipFromAPI(conn *libvirt.Connect, domain, network string) (string, error) {
mac, err := macFromXML(conn, domain, network)
if err != nil { if err != nil {
return "", errors.Wrap(err, "reading status file") return "", fmt.Errorf("failed getting MAC address: %w", err)
} }
return parseStatusAndReturnIP(d.PrivateMAC, statuses) ifaces, err := ifListFromAPI(conn, domain)
if err != nil {
return "", fmt.Errorf("failed getting network %s interfaces using API of domain %s: %w", network, domain, err)
}
for _, i := range ifaces {
if i.Hwaddr == mac {
if i.Addrs != nil {
log.Debugf("domain %s has current primary IP address %s and MAC address %s in network %s", domain, i.Addrs[0].Addr, mac, network)
return i.Addrs[0].Addr, nil
}
log.Debugf("domain %s with MAC address %s doesn't have current IP address in network %s: %+v", domain, mac, network, i)
return "", nil
}
}
log.Debugf("unable to find current IP address of domain %s in network %s", domain, network)
return "", nil
} }
func parseStatusAndReturnIP(privateMAC string, statuses []byte) (string, error) { // ifListFromAPI returns current domain interfaces.
type StatusEntry struct { func ifListFromAPI(conn *libvirt.Connect, domain string) ([]libvirt.DomainInterface, error) {
IPAddress string `json:"ip-address"` dom, err := conn.LookupDomainByName(domain)
MacAddress string `json:"mac-address"` if err != nil {
return nil, fmt.Errorf("failed looking up domain %s: %w", domain, err)
}
defer func() { _ = dom.Free() }()
ifs, err := dom.ListAllInterfaceAddresses(libvirt.DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE)
if err != nil {
return nil, fmt.Errorf("failed listing network interface addresses of domain %s: %w", domain, err)
} }
var statusEntries []StatusEntry
// empty file return blank return ifs, nil
if len(statuses) == 0 { }
// ipFromXML returns defined IP address of interface in network.
func ipFromXML(conn *libvirt.Connect, domain, network string) (string, error) {
mac, err := macFromXML(conn, domain, network)
if err != nil {
return "", fmt.Errorf("failed getting MAC address: %w", err)
}
lease, err := dhcpLease(conn, network, "", mac, "")
if err != nil {
return "", fmt.Errorf("failed looking up network %s for host DHCP lease {name: <any>, mac: %q, ip: <any>}: %w", network, mac, err)
}
if lease == nil {
log.Debugf("unable to find defined IP address of network %s interface with MAC address %s", network, mac)
return "", nil return "", nil
} }
err := json.Unmarshal(statuses, &statusEntries) log.Debugf("domain %s has defined IP address %s and MAC address %s in network %s", domain, lease.IPaddr, mac, network)
return lease.IPaddr, nil
}
// macFromXML returns defined MAC address of interface in network from domain XML.
func macFromXML(conn *libvirt.Connect, domain, network string) (string, error) {
domIfs, err := ifListFromXML(conn, domain)
if err != nil { if err != nil {
return "", errors.Wrap(err, "reading status file") return "", fmt.Errorf("failed getting network %s interfaces using XML of domain %s: %w", network, domain, err)
} }
for _, status := range statusEntries { for _, i := range domIfs {
if status.MacAddress == privateMAC { if i.Source.Network == network {
return status.IPAddress, nil log.Debugf("domain %s has defined MAC address %s in network %s", domain, i.Mac.Address, network)
return i.Mac.Address, nil
} }
} }
return "", nil return "", fmt.Errorf("unable to get defined MAC address of network %s interface using XML of domain %s: network %s not found", network, domain, network)
} }
func (d *Driver) lookupIPFromLeasesFile() (string, error) { // ifListFromXML returns defined domain interfaces from domain XML.
leasesFile := fmt.Sprintf("/var/lib/libvirt/dnsmasq/%s.leases", d.PrivateNetwork) func ifListFromXML(conn *libvirt.Connect, domain string) ([]kvmIface, error) {
leases, err := ioutil.ReadFile(leasesFile) dom, err := conn.LookupDomainByName(domain)
if err != nil { if err != nil {
return "", errors.Wrap(err, "reading leases file") return nil, fmt.Errorf("failed looking up domain %s: %w", domain, err)
} }
ipAddress := "" defer func() { _ = dom.Free() }()
for _, lease := range strings.Split(string(leases), "\n") {
if len(lease) == 0 { domXML, err := dom.GetXMLDesc(0)
continue if err != nil {
} return nil, fmt.Errorf("failed getting XML of domain %s: %w", domain, err)
// format for lease entry }
// ExpiryTime MAC IP Hostname ExtendedMAC
entry := strings.Split(lease, " ") var d struct {
if len(entry) != 5 { Interfaces []kvmIface `xml:"devices>interface"`
return "", fmt.Errorf("malformed leases entry: %s", entry)
}
if entry[1] == d.PrivateMAC {
ipAddress = entry[2]
}
} }
return ipAddress, nil err = xml.Unmarshal([]byte(domXML), &d)
if err != nil {
return nil, fmt.Errorf("failed parsing XML of domain %s: %w", domain, err)
}
return d.Interfaces, nil
} }
// +build linux
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kvm
import (
"testing"
)
var (
emptyFile = []byte(``)
fileWithInvalidJSON = []byte(`{`)
fileWithNoStatus = []byte(`[
]`)
fileWithStatus = []byte(`[
{
"ip-address": "1.2.3.5",
"mac-address": "a4:b5:c6:d7:e8:f9",
"hostname": "host2",
"client-id": "01:44:59:e7:fd:f4:d6",
"expiry-time": 1558638717
},
{
"ip-address": "1.2.3.4",
"mac-address": "a1:b2:c3:d4:e5:f6",
"hostname": "host1",
"client-id": "01:ec:97:de:a2:86:81",
"expiry-time": 1558639092
}
]`)
)
func TestParseStatusAndReturnIp(t *testing.T) {
type args struct {
mac string
statuses []byte
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
"emptyFile",
args{"a1:b2:c3:d4:e5:f6", emptyFile},
"",
false,
},
{
"fileWithStatus",
args{"a1:b2:c3:d4:e5:f6", fileWithStatus},
"1.2.3.4",
false,
},
{
"fileWithNoStatus",
args{"a4:b5:c6:d7:e8:f9", fileWithNoStatus},
"",
false,
},
{
"fileWithInvalidJSON",
args{"a4:b5:c6:d7:e8:f9", fileWithInvalidJSON},
"",
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseStatusAndReturnIP(tt.args.mac, tt.args.statuses)
if (err != nil) != tt.wantErr {
t.Errorf("parseStatusAndReturnIP() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("parseStatusAndReturnIP() = %v, want %v", got, tt.want)
}
})
}
}
...@@ -52,11 +52,11 @@ type ClusterConfig struct { ...@@ -52,11 +52,11 @@ type ClusterConfig struct {
HypervVirtualSwitch string HypervVirtualSwitch string
HypervUseExternalSwitch bool HypervUseExternalSwitch bool
HypervExternalAdapter string HypervExternalAdapter string
KVMNetwork string // Only used by the KVM driver KVMNetwork string // Only used by the KVM2 driver
KVMQemuURI string // Only used by kvm2 KVMQemuURI string // Only used by the KVM2 driver
KVMGPU bool // Only used by kvm2 KVMGPU bool // Only used by the KVM2 driver
KVMHidden bool // Only used by kvm2 KVMHidden bool // Only used by the KVM2 driver
KVMNUMACount int // Only used by kvm2 KVMNUMACount int // Only used by the KVM2 driver
DockerOpt []string // Each entry is formatted as KEY=VALUE. DockerOpt []string // Each entry is formatted as KEY=VALUE.
DisableDriverMounts bool // Only used by virtualbox DisableDriverMounts bool // Only used by virtualbox
NFSShare []string NFSShare []string
......
...@@ -141,6 +141,11 @@ func IsMock(name string) bool { ...@@ -141,6 +141,11 @@ func IsMock(name string) bool {
return name == Mock return name == Mock
} }
// IsKVM checks if the driver is a KVM[2]
func IsKVM(name string) bool {
return name == KVM2 || name == AliasKVM
}
// IsVM checks if the driver is a VM // IsVM checks if the driver is a VM
func IsVM(name string) bool { func IsVM(name string) bool {
if IsKIC(name) || BareMetal(name) { if IsKIC(name) || BareMetal(name) {
......
...@@ -83,7 +83,7 @@ func configure(cc config.ClusterConfig, n config.Node) (interface{}, error) { ...@@ -83,7 +83,7 @@ func configure(cc config.ClusterConfig, n config.Node) (interface{}, error) {
Memory: cc.Memory, Memory: cc.Memory,
CPU: cc.CPUs, CPU: cc.CPUs,
Network: cc.KVMNetwork, Network: cc.KVMNetwork,
PrivateNetwork: "minikube-net", PrivateNetwork: privateNetwork(cc),
Boot2DockerURL: download.LocalISOResource(cc.MinikubeISO), Boot2DockerURL: download.LocalISOResource(cc.MinikubeISO),
DiskSize: cc.DiskSize, DiskSize: cc.DiskSize,
DiskPath: filepath.Join(localpath.MiniPath(), "machines", name, fmt.Sprintf("%s.rawdisk", name)), DiskPath: filepath.Join(localpath.MiniPath(), "machines", name, fmt.Sprintf("%s.rawdisk", name)),
...@@ -95,6 +95,14 @@ func configure(cc config.ClusterConfig, n config.Node) (interface{}, error) { ...@@ -95,6 +95,14 @@ func configure(cc config.ClusterConfig, n config.Node) (interface{}, error) {
}, nil }, nil
} }
// if network is not user-defined it defaults to "mk-<cluster_name>"
func privateNetwork(cc config.ClusterConfig) string {
if cc.Network == "" {
return fmt.Sprintf("mk-%s", cc.KubernetesConfig.ClusterName)
}
return cc.Network
}
// defaultURI returns the QEMU URI to connect to for health checks // defaultURI returns the QEMU URI to connect to for health checks
func defaultURI() string { func defaultURI() string {
u := os.Getenv("LIBVIRT_DEFAULT_URI") u := os.Getenv("LIBVIRT_DEFAULT_URI")
......
...@@ -23,7 +23,6 @@ import ( ...@@ -23,7 +23,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/pkg/errors"
"k8s.io/klog/v2" "k8s.io/klog/v2"
) )
...@@ -59,12 +58,13 @@ type reservation struct { ...@@ -59,12 +58,13 @@ type reservation struct {
// Parameters contains main network parameters. // Parameters contains main network parameters.
type Parameters struct { type Parameters struct {
IP string // IP address of the network IP string // IP address of network
Netmask string // form: 4-byte ('a.b.c.d') Netmask string // dotted-decimal format ('a.b.c.d')
CIDR string // form: CIDR Prefix int // network prefix length (number of leading ones in network mask)
Gateway string // first IP address (assumed, not checked !) CIDR string // CIDR format ('a.b.c.d/n')
Gateway string // taken from network interface address or assumed as first network IP address from given addr
ClientMin string // second IP address ClientMin string // second IP address
ClientMax string // last IP address before broadcastS ClientMax string // last IP address before broadcast
Broadcast string // last IP address Broadcast string // last IP address
Interface Interface
} }
...@@ -77,9 +77,9 @@ type Interface struct { ...@@ -77,9 +77,9 @@ type Interface struct {
IfaceMAC string IfaceMAC string
} }
// inspect initialises IPv4 network parameters struct from given address. // inspect initialises IPv4 network parameters struct from given address addr.
// address can be single address (like "192.168.17.42"), network address (like "192.168.17.0"), or in cidr form (like "192.168.17.42/24 or "192.168.17.0/24"). // addr can be single address (like "192.168.17.42"), network address (like "192.168.17.0") or in CIDR form (like "192.168.17.42/24 or "192.168.17.0/24").
// If addr is valid existsing interface address, network struct will also contain info about the respective interface. // If addr belongs to network of local network interface, parameters will also contain info about that network interface.
func inspect(addr string) (*Parameters, error) { func inspect(addr string) (*Parameters, error) {
n := &Parameters{} n := &Parameters{}
...@@ -88,21 +88,24 @@ func inspect(addr string) (*Parameters, error) { ...@@ -88,21 +88,24 @@ func inspect(addr string) (*Parameters, error) {
if err != nil { if err != nil {
ip = net.ParseIP(addr) ip = net.ParseIP(addr)
if ip == nil { if ip == nil {
return nil, errors.Wrapf(err, "parsing address %q", addr) return nil, fmt.Errorf("failed parsing address %s: %w", addr, err)
} }
} }
// check local interfaces // check local network interfaces
ifaces, _ := net.Interfaces() ifaces, err := net.Interfaces()
if err != nil {
return nil, fmt.Errorf("failed listing network interfaces: %w", err)
}
for _, iface := range ifaces { for _, iface := range ifaces {
ifAddrs, err := iface.Addrs() ifAddrs, err := iface.Addrs()
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "listing addresses of network interface %+v", iface) return nil, fmt.Errorf("failed listing addresses of network interface %+v: %w", iface, err)
} }
for _, ifAddr := range ifAddrs { for _, ifAddr := range ifAddrs {
ifip, lan, err := net.ParseCIDR(ifAddr.String()) ifip, lan, err := net.ParseCIDR(ifAddr.String())
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "parsing address of network iface %+v", ifAddr) return nil, fmt.Errorf("failed parsing network interface address %+v: %w", ifAddr, err)
} }
if lan.Contains(ip) { if lan.Contains(ip) {
n.IfaceName = iface.Name n.IfaceName = iface.Name
...@@ -116,6 +119,7 @@ func inspect(addr string) (*Parameters, error) { ...@@ -116,6 +119,7 @@ func inspect(addr string) (*Parameters, error) {
} }
} }
// couldn't determine network parameters from addr nor from network interfaces
if network == nil { if network == nil {
ipnet := &net.IPNet{ ipnet := &net.IPNet{
IP: ip, IP: ip,
...@@ -123,15 +127,16 @@ func inspect(addr string) (*Parameters, error) { ...@@ -123,15 +127,16 @@ func inspect(addr string) (*Parameters, error) {
} }
_, network, err = net.ParseCIDR(ipnet.String()) _, network, err = net.ParseCIDR(ipnet.String())
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "determining network address from %q", addr) return nil, fmt.Errorf("failed determining address of network from %s: %w", addr, err)
} }
} }
n.IP = network.IP.String() n.IP = network.IP.String()
n.Netmask = net.IP(network.Mask).String() // form: 4-byte ('a.b.c.d') n.Netmask = net.IP(network.Mask).String() // dotted-decimal format ('a.b.c.d')
n.Prefix, _ = network.Mask.Size()
n.CIDR = network.String() n.CIDR = network.String()
networkIP := binary.BigEndian.Uint32(network.IP) // IP address of the network networkIP := binary.BigEndian.Uint32(network.IP) // IP address of network
networkMask := binary.BigEndian.Uint32(network.Mask) // network mask networkMask := binary.BigEndian.Uint32(network.Mask) // network mask
broadcastIP := (networkIP & networkMask) | (networkMask ^ 0xffffffff) // last network IP address broadcastIP := (networkIP & networkMask) | (networkMask ^ 0xffffffff) // last network IP address
...@@ -161,14 +166,14 @@ func inspect(addr string) (*Parameters, error) { ...@@ -161,14 +166,14 @@ func inspect(addr string) (*Parameters, error) {
// isSubnetTaken returns if local network subnet exists and any error occurred. // isSubnetTaken returns if local network subnet exists and any error occurred.
// If will return false in case of an error. // If will return false in case of an error.
func isSubnetTaken(subnet string) (bool, error) { func isSubnetTaken(subnet string) (bool, error) {
ips, err := net.InterfaceAddrs() ifAddrs, err := net.InterfaceAddrs()
if err != nil { if err != nil {
return false, errors.Wrap(err, "listing local networks") return false, fmt.Errorf("failed listing network interface addresses: %w", err)
} }
for _, ip := range ips { for _, ifAddr := range ifAddrs {
_, lan, err := net.ParseCIDR(ip.String()) _, lan, err := net.ParseCIDR(ifAddr.String())
if err != nil { if err != nil {
return false, errors.Wrapf(err, "parsing network iface address %q", ip) return false, fmt.Errorf("failed parsing network interface address %+v: %w", ifAddr, err)
} }
if lan.Contains(net.ParseIP(subnet)) { if lan.Contains(net.ParseIP(subnet)) {
return true, nil return true, nil
...@@ -177,7 +182,7 @@ func isSubnetTaken(subnet string) (bool, error) { ...@@ -177,7 +182,7 @@ func isSubnetTaken(subnet string) (bool, error) {
return false, nil return false, nil
} }
// isSubnetPrivate returns if subnet is a private network. // isSubnetPrivate returns if subnet is private network.
func isSubnetPrivate(subnet string) bool { func isSubnetPrivate(subnet string) bool {
for _, ipnet := range privateSubnets { for _, ipnet := range privateSubnets {
if ipnet.Contains(net.ParseIP(subnet)) { if ipnet.Contains(net.ParseIP(subnet)) {
...@@ -212,9 +217,9 @@ func FreeSubnet(startSubnet string, step, tries int) (*Parameters, error) { ...@@ -212,9 +217,9 @@ func FreeSubnet(startSubnet string, step, tries int) (*Parameters, error) {
} else { } else {
klog.Infof("skipping subnet %s that is not private", n.CIDR) klog.Infof("skipping subnet %s that is not private", n.CIDR)
} }
ones, _ := net.ParseIP(n.IP).DefaultMask().Size() prefix, _ := net.ParseIP(n.IP).DefaultMask().Size()
nextSubnet := net.ParseIP(startSubnet).To4() nextSubnet := net.ParseIP(startSubnet).To4()
if ones <= 16 { if prefix <= 16 {
nextSubnet[1] += byte(step) nextSubnet[1] += byte(step)
} else { } else {
nextSubnet[2] += byte(step) nextSubnet[2] += byte(step)
......
...@@ -69,7 +69,7 @@ minikube start [flags] ...@@ -69,7 +69,7 @@ minikube start [flags]
--kubernetes-version string The Kubernetes version that the minikube VM will use (ex: v1.2.3, 'stable' for v1.20.2, 'latest' for v1.20.5-rc.0). Defaults to 'stable'. --kubernetes-version string The Kubernetes version that the minikube VM will use (ex: v1.2.3, 'stable' for v1.20.2, 'latest' for v1.20.5-rc.0). Defaults to 'stable'.
--kvm-gpu Enable experimental NVIDIA GPU support in minikube --kvm-gpu Enable experimental NVIDIA GPU support in minikube
--kvm-hidden Hide the hypervisor signature from the guest in minikube (kvm2 driver only) --kvm-hidden Hide the hypervisor signature from the guest in minikube (kvm2 driver only)
--kvm-network string The KVM network name. (kvm2 driver only) (default "default") --kvm-network string The KVM default network name. (kvm2 driver only) (default "default")
--kvm-numa-count int Simulate numa node count in minikube, supported numa node count range is 1-8 (kvm2 driver only) (default 1) --kvm-numa-count int Simulate numa node count in minikube, supported numa node count range is 1-8 (kvm2 driver only) (default 1)
--kvm-qemu-uri string The KVM QEMU connection URI. (kvm2 driver only) (default "qemu:///system") --kvm-qemu-uri string The KVM QEMU connection URI. (kvm2 driver only) (default "qemu:///system")
--listen-address string IP Address to use to expose ports (docker and podman driver only) --listen-address string IP Address to use to expose ports (docker and podman driver only)
...@@ -79,7 +79,7 @@ minikube start [flags] ...@@ -79,7 +79,7 @@ minikube start [flags]
--namespace string The named space to activate after start (default "default") --namespace string The named space to activate after start (default "default")
--nat-nic-type string NIC Type used for nat network. One of Am79C970A, Am79C973, 82540EM, 82543GC, 82545EM, or virtio (virtualbox driver only) (default "virtio") --nat-nic-type string NIC Type used for nat network. One of Am79C970A, Am79C973, 82540EM, 82543GC, 82545EM, or virtio (virtualbox driver only) (default "virtio")
--native-ssh Use native Golang SSH client (default true). Set to 'false' to use the command line 'ssh' command when accessing the docker machine. Useful for the machine drivers when they will not start with 'Waiting for SSH'. (default true) --native-ssh Use native Golang SSH client (default true). Set to 'false' to use the command line 'ssh' command when accessing the docker machine. Useful for the machine drivers when they will not start with 'Waiting for SSH'. (default true)
--network string network to run minikube with. Only available with the docker/podman drivers. If left empty, minikube will create a new network. --network string network to run minikube with. Now it is used by docker/podman and KVM drivers. If left empty, minikube will create a new network.
--network-plugin string Kubelet network plug-in to use (default: auto) --network-plugin string Kubelet network plug-in to use (default: auto)
--nfs-share strings Local folders to share with Guest via NFS mounts (hyperkit driver only) --nfs-share strings Local folders to share with Guest via NFS mounts (hyperkit driver only)
--nfs-shares-root string Where to root the NFS Shares, defaults to /nfsshares (hyperkit driver only) (default "/nfsshares") --nfs-shares-root string Where to root the NFS Shares, defaults to /nfsshares (hyperkit driver only) (default "/nfsshares")
......
...@@ -20,11 +20,12 @@ aliases: ...@@ -20,11 +20,12 @@ aliases:
## Special features ## Special features
The `minikube start` command supports 3 additional kvm specific flags: The `minikube start` command supports 5 additional KVM specific flags:
* **`--gpu`**: Enable experimental NVIDIA GPU support in minikube * **`--gpu`**: Enable experimental NVIDIA GPU support in minikube
* **`--hidden`**: Hide the hypervisor signature from the guest in minikube * **`--hidden`**: Hide the hypervisor signature from the guest in minikube
* **`--kvm-network`**: The KVM network name * **`--kvm-network`**: The KVM default network name
* **`--network`**: The dedicated KVM private network name
* **`--kvm-qemu-uri`**: The KVM qemu uri, defaults to qemu:///system * **`--kvm-qemu-uri`**: The KVM qemu uri, defaults to qemu:///system
## Issues ## Issues
...@@ -44,6 +45,7 @@ If you are running KVM in a nested virtualization environment ensure your config ...@@ -44,6 +45,7 @@ If you are running KVM in a nested virtualization environment ensure your config
## Troubleshooting ## Troubleshooting
* Run `id` to confirm that user belongs to the libvirt[d] group (the output should contain entry similar to: 'groups=...,108(libvirt),...'). * Run `id` to confirm that user belongs to the libvirt[d] group (the output should contain entry similar to: 'groups=...,108(libvirt),...').
* Run `virsh domcapabilities --virttype="kvm"` to confirm that the host supports KVM virtualisation.
* Run `virt-host-validate` and check for the suggestions. * Run `virt-host-validate` and check for the suggestions.
* Run ``ls -la `which virsh` ``, `virsh uri`, `sudo virsh net-list --all` and `ip a s` to collect additional information for debugging. * Run ``ls -la `which virsh` ``, `virsh uri`, `sudo virsh net-list --all` and `ip a s` to collect additional information for debugging.
* Run `minikube start --alsologtostderr -v=9` to debug crashes. * Run `minikube start --alsologtostderr -v=9` to debug crashes.
...@@ -70,7 +72,7 @@ where: ...@@ -70,7 +72,7 @@ where:
* ***default*** is the default libvirt network, * ***default*** is the default libvirt network,
* ***mk-kvm0*** is a default libvirt network created for minikube ***kvm0*** profile (eg, using `minikube start -p kvm0 --driver=kvm2`), * ***mk-kvm0*** is a default libvirt network created for minikube ***kvm0*** profile (eg, using `minikube start -p kvm0 --driver=kvm2`),
* ***mk-minikube*** is a network created for default minikube profile (eg, using `minikube start --driver=kvm2`) and * ***mk-minikube*** is a network created for default minikube profile (eg, using `minikube start --driver=kvm2`) and
* ***my-custom-kvm-priv-net*** is a custom network name provided for minikube profile (eg, using `minikube start -p kvm1 --driver=kvm2 --kvm-private-network="my-custom-kvm-priv-net"`). * ***my-custom-kvm-priv-net*** is a custom private network name provided for minikube profile (eg, using `minikube start -p kvm1 --driver=kvm2 --network="my-custom-kvm-priv-net"`).
2. Run `sudo virsh net-autostart <network>` to manually set **network** to autostart, if not already set. 2. Run `sudo virsh net-autostart <network>` to manually set **network** to autostart, if not already set.
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
"- Ensure your {{.driver_name}} daemon has access to enough CPU/memory resources.": "", "- Ensure your {{.driver_name}} daemon has access to enough CPU/memory resources.": "",
"- Prune unused {{.driver_name}} images, volumes, networks and abandoned containers.\n\n\t\t\t\t{{.driver_name}} system prune --volumes": "", "- Prune unused {{.driver_name}} images, volumes, networks and abandoned containers.\n\n\t\t\t\t{{.driver_name}} system prune --volumes": "",
"- Restart your {{.driver_name}} service": "{{.driver_name}} 서비스를 다시 시작하세요", "- Restart your {{.driver_name}} service": "{{.driver_name}} 서비스를 다시 시작하세요",
"--network flag is only valid with the docker/podman drivers, it will be ignored": "", "--network flag is only valid with the docker/podman and KVM drivers, it will be ignored": "",
"A set of apiserver IP Addresses which are used in the generated certificate for kubernetes. This can be used if you want to make the apiserver available from outside the machine": "", "A set of apiserver IP Addresses which are used in the generated certificate for kubernetes. This can be used if you want to make the apiserver available from outside the machine": "",
"A set of apiserver names which are used in the generated certificate for kubernetes. This can be used if you want to make the apiserver available from outside the machine": "", "A set of apiserver names which are used in the generated certificate for kubernetes. This can be used if you want to make the apiserver available from outside the machine": "",
"A set of key=value pairs that describe feature gates for alpha/experimental features.": "", "A set of key=value pairs that describe feature gates for alpha/experimental features.": "",
...@@ -687,7 +687,7 @@ ...@@ -687,7 +687,7 @@
"mount failed": "마운트 실패", "mount failed": "마운트 실패",
"namespaces to pause": "잠시 멈추려는 네임스페이스", "namespaces to pause": "잠시 멈추려는 네임스페이스",
"namespaces to unpause": "재개하려는 네임스페이스", "namespaces to unpause": "재개하려는 네임스페이스",
"network to run minikube with. Only available with the docker/podman drivers. If left empty, minikube will create a new network.": "", "network to run minikube with. Now it is used by docker/podman and KVM drivers. If left empty, minikube will create a new network.": "",
"none driver does not support multi-node clusters": "", "none driver does not support multi-node clusters": "",
"not enough arguments ({{.ArgCount}}).\\nusage: minikube config set PROPERTY_NAME PROPERTY_VALUE": "", "not enough arguments ({{.ArgCount}}).\\nusage: minikube config set PROPERTY_NAME PROPERTY_VALUE": "",
"output layout (EXPERIMENTAL, JSON only): 'nodes' or 'cluster'": "", "output layout (EXPERIMENTAL, JSON only): 'nodes' or 'cluster'": "",
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
"- Ensure your {{.driver_name}} daemon has access to enough CPU/memory resources.": "", "- Ensure your {{.driver_name}} daemon has access to enough CPU/memory resources.": "",
"- Prune unused {{.driver_name}} images, volumes, networks and abandoned containers.\n\n\t\t\t\t{{.driver_name}} system prune --volumes": "", "- Prune unused {{.driver_name}} images, volumes, networks and abandoned containers.\n\n\t\t\t\t{{.driver_name}} system prune --volumes": "",
"- Restart your {{.driver_name}} service": "", "- Restart your {{.driver_name}} service": "",
"--network flag is only valid with the docker/podman drivers, it will be ignored": "", "--network flag is only valid with the docker/podman and KVM drivers, it will be ignored": "",
"A set of apiserver IP Addresses which are used in the generated certificate for kubernetes. This can be used if you want to make the apiserver available from outside the machine": "", "A set of apiserver IP Addresses which are used in the generated certificate for kubernetes. This can be used if you want to make the apiserver available from outside the machine": "",
"A set of apiserver names which are used in the generated certificate for kubernetes. This can be used if you want to make the apiserver available from outside the machine": "", "A set of apiserver names which are used in the generated certificate for kubernetes. This can be used if you want to make the apiserver available from outside the machine": "",
"A set of key=value pairs that describe feature gates for alpha/experimental features.": "", "A set of key=value pairs that describe feature gates for alpha/experimental features.": "",
...@@ -613,7 +613,7 @@ ...@@ -613,7 +613,7 @@
"mount failed": "", "mount failed": "",
"namespaces to pause": "", "namespaces to pause": "",
"namespaces to unpause": "", "namespaces to unpause": "",
"network to run minikube with. Only available with the docker/podman drivers. If left empty, minikube will create a new network.": "", "network to run minikube with. Now it is used by docker/podman and KVM drivers. If left empty, minikube will create a new network.": "",
"none driver does not support multi-node clusters": "", "none driver does not support multi-node clusters": "",
"not enough arguments ({{.ArgCount}}).\\nusage: minikube config set PROPERTY_NAME PROPERTY_VALUE": "", "not enough arguments ({{.ArgCount}}).\\nusage: minikube config set PROPERTY_NAME PROPERTY_VALUE": "",
"output layout (EXPERIMENTAL, JSON only): 'nodes' or 'cluster'": "", "output layout (EXPERIMENTAL, JSON only): 'nodes' or 'cluster'": "",
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册