Skip to content

Commit

Permalink
Allow hostonlynets networks of any name to be created
Browse files Browse the repository at this point in the history
With VirtualBox v7 on macOS we can create networks of 'hostonlynets' with a
new CLI experience that allows any name for the created network.

We leverage this new possibility, while still retaining the former feature
with networks of 'hostonlyif' type, when the name of a non-existing and
to-be-created network could possibly be guessed in advance, and if correctly
guessed, then everything was fine.
  • Loading branch information
bgandon committed Feb 16, 2024
1 parent 7f024d5 commit 2f8f41d
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 108 deletions.
15 changes: 8 additions & 7 deletions src/bosh-virtualbox-cpi/vm/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ func (h Host) FindNetwork(net Network) (bnet.Network, error) {
}
}

// Enable the network that the VM is to be attached to, and create that
// network if necessary.
//
// In the VM creation workflow, the network is enabled _before_ being looked
// up with (vm.Host).FindNetworks(Network).
func (h Host) EnableNetworks(nets Networks) error {
for _, net := range nets {
switch net.CloudPropertyType() {
Expand Down Expand Up @@ -86,6 +91,8 @@ func (n *hostNetwork) Find() (bnet.Network, error) {
return nil, fmt.Errorf("Expected to find network '%s'", n.net.CloudPropertyName())
}

// Enable the network, and try to create it if it does not exist and if is not
// already been tried.
func (n *hostNetwork) Enable() error {
actualNets, err := n.adapter.List()
if err != nil {
Expand Down Expand Up @@ -189,13 +196,7 @@ func (n hostOnlysAdapter) List() ([]bnet.Network, error) {
}

func (n hostOnlysAdapter) Create(net Network) error {
canCreate, err := n.AddHostOnly(net.CloudPropertyName(), net.Gateway(), net.Netmask())
if err != nil {
return err
} else if !canCreate {
return fmt.Errorf("Expected to find Host-only network '%s'", net.CloudPropertyName())
}
return nil
return n.AddHostOnly(net.CloudPropertyName(), net.Gateway(), net.Netmask())
}

func (n hostOnlysAdapter) Matches(net Network, actualNet bnet.Network) bool {
Expand Down
172 changes: 72 additions & 100 deletions src/bosh-virtualbox-cpi/vm/network/add_host_only.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,121 +10,99 @@ import (
)

var (
createdHostOnlyMatch = regexp.MustCompile(`Interface '(.+)' was successfully created`)
createdHostOnlyNetMatch = regexp.MustCompile(`Name: vboxnet0`)
createdHostOnlyMatch = regexp.MustCompile(`Interface '(.+)' was successfully created`)
)

func (n Networks) AddHostOnly(name, gateway, netmask string) (bool, error) {
// VB does not allow naming host-only networks inside version <= 6 , exit if it's not the first one
if len(name) > 0 && name != "vboxnet0" {
return false, nil
}

createdName, err := n.createHostOnly(gateway, netmask)

func (n Networks) AddHostOnly(expectedName, gateway, netmask string) error {
systemInfo, err := n.NewSystemInfo()
if err != nil {
return true, err
return err
}

if len(name) > 0 && createdName != name {
n.cleanUpPartialHostOnlyCreate(createdName)
return true, fmt.Errorf("expected created host-only network '%s' to have name '%s'", createdName, name)
var createdName string
if systemInfo.IsMacOSVboxV7OrLater() {
if err := n.createHostOnlyNet(expectedName, gateway, netmask); err != nil {
return err
}
createdName = expectedName
} else {
// Virtualox v6 or earlier choses itself the name of newly created
// host-only networks
createdName, err := n.createLegacyHostOnly()
if err != nil {
return err
}
if len(expectedName) > 0 && createdName != expectedName {
n.cleanUpPartialHostOnlyCreate(createdName)
return fmt.Errorf("expected created host-only network '%s' to have name '%s'. Have you made an incorrect guess?", createdName, expectedName)
}
}

err = n.configureHostOnly(createdName, gateway, netmask)
if err != nil {
n.cleanUpPartialHostOnlyCreate(createdName)
return true, err
return err
}

return true, nil
return nil
}

func (n Networks) createHostOnly(gateway, netmask string) (string, error) {
func (n Networks) createHostOnlyNet(name, gateway, netmask string) error {
systemInfo, err := n.NewSystemInfo()
if err != nil {
return "", err
return err
}
maskIP := net.ParseIP(netmask).To4()
if maskIP == nil {
return bosherr.Errorf("expected netmask to be valid IP v4 (got '%s')", netmask)
}
gwIP := net.ParseIP(gateway)
if gwIP == nil {
return bosherr.Errorf("expected gateway to be valid IP v4 (got '%s')", gateway)
}

var matches []string
var errorMessage string
var expectedMatchesLen int

if systemInfo.IsMacOSVboxV7OrLater() {
maskIP := net.ParseIP(netmask).To4()
if maskIP == nil {
return "", bosherr.Errorf("expected netmask to be valid IP v4 (got '%s')", netmask)
}
gwIP := net.ParseIP(gateway)
if gwIP == nil {
return "", bosherr.Errorf("expected gateway to be valid IP v4 (got '%s')", gateway)
}

mask := net.IPv4Mask(maskIP[0], maskIP[1], maskIP[2], maskIP[3])
subnetFirstIP := &net.IPNet{
IP: gwIP,
Mask: mask,
}
maskLength, _ := mask.Size()
_, subnet, _ := net.ParseCIDR(fmt.Sprintf("%s/%v", gateway, maskLength))

lowerIp, err := systemInfo.GetFirstIP(subnetFirstIP)
if err != nil {
return "", err
}
upperIp, err := systemInfo.GetLastIP(subnet)
if err != nil {
return "", err
}

args := []string{"hostonlynet",
"add", fmt.Sprintf("--name=%s", "vboxnet0"),
fmt.Sprintf("--netmask=%s", netmask), fmt.Sprintf("--lower-ip=%s", lowerIp.String()),
fmt.Sprintf("--upper-ip=%s", upperIp.String()), "--disable"}
mask := net.IPv4Mask(maskIP[0], maskIP[1], maskIP[2], maskIP[3])
subnetFirstIP := &net.IPNet{
IP: gwIP,
Mask: mask,
}
maskLength, _ := mask.Size()
_, subnet, _ := net.ParseCIDR(fmt.Sprintf("%s/%v", gateway, maskLength))

// The output of the hostonlynet interface creation is empty. We need another solution to handle and verify the
// VboxManage creation.
_, err = n.driver.ExecuteComplex(args, driver.ExecuteOpts{})
if err != nil {
return "", err
}
var lowerIP, upperIP net.IP
if lowerIP, err = systemInfo.GetFirstIP(subnetFirstIP); err != nil {
return err
}
if upperIP, err = systemInfo.GetLastIP(subnet); err != nil {
return err
}

args = []string{"list", "hostonlynets"}
output, err := n.driver.ExecuteComplex(args, driver.ExecuteOpts{})
if err != nil {
return "", err
}
args := []string{"hostonlynet",
"add", fmt.Sprintf("--name=%s", name),
fmt.Sprintf("--netmask=%s", netmask), fmt.Sprintf("--lower-ip=%s", lowerIP.String()),
fmt.Sprintf("--upper-ip=%s", upperIP.String()), "--disable"}

matches = createdHostOnlyNetMatch.FindStringSubmatch(output)
//Define the return value of the created Host only Adapter. We're only creating one adapter,
//so we can also define the used name hard coded.
if len(matches) == 1 {
matches[0] = "vboxnet0"
}
// The output of the hostonlynet interface creation is empty. We need another solution to handle and verify the
// VboxManage creation.
_, err = n.driver.ExecuteComplex(args, driver.ExecuteOpts{})
if err != nil {
return err
}
return nil
}

errorMessage = fmt.Sprintf(
"Internal inconsistency: Expected len(%s matches) == 1:",
createdHostOnlyNetMatch,
)
expectedMatchesLen = 1
} else {
output, err := n.driver.Execute("hostonlyif", "create")
if err != nil {
return "", err
}
matches = createdHostOnlyMatch.FindStringSubmatch(output)
errorMessage = fmt.Sprintf(
"Internal inconsistency: Expected len(%s matches) == 2:",
createdHostOnlyMatch,
)
expectedMatchesLen = 2
func (n Networks) createLegacyHostOnly() (string, error) {
output, err := n.driver.Execute("hostonlyif", "create")
if err != nil {
return "", err
}

if len(matches) != expectedMatchesLen {
panic(errorMessage)
matches := createdHostOnlyMatch.FindStringSubmatch(output)
if len(matches) != 2 {
panic(fmt.Sprintf("Internal inconsistency: Expected len(%s matches) == 2:", createdHostOnlyMatch))
}

return matches[expectedMatchesLen-1], nil
return matches[1], nil
}

func (n Networks) configureHostOnly(name, gateway, netmask string) error {
Expand Down Expand Up @@ -157,17 +135,11 @@ func (n Networks) cleanUpPartialHostOnlyCreate(name string) {
"Failed to get the SystemInfo: %s", err)
}

args := []string{
"hostonlyif",
"remove",
name,
}
if systemInfo.IsMacOSVbox7() {
args = []string{
"hostonlynet",
"remove",
fmt.Sprintf("--name=%s", name),
}
args := []string{"hostonlyif", "remove"}
if systemInfo.IsMacOSVboxV7OrLater() {
args = append(args, fmt.Sprintf("--name=%s", name))
} else {
args = append(args, name)
}

_, err = n.driver.ExecuteComplex(args, driver.ExecuteOpts{})
Expand Down
4 changes: 3 additions & 1 deletion src/bosh-virtualbox-cpi/vm/network/system_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"net"
"runtime"
"strings"

bosherr "github.com/cloudfoundry/bosh-utils/errors"
)

type SystemInfo struct {
Expand Down Expand Up @@ -57,7 +59,7 @@ func (n Networks) getVboxVersion() (string, string, error) {
matches := strings.Split(output, ".")

if len(matches) > 3 {
panic(fmt.Sprintf("Internal inconsistency: Expected len(%s matches) >= 3:", createdHostOnlyMatch))
return "", "", bosherr.Errorf("Expected VirtualBox version to have 3 dot-separated parts (got '%s')", output)
}

return matches[0], matches[1], nil
Expand Down

0 comments on commit 2f8f41d

Please sign in to comment.