//go:build !windows
// +build !windows

package net_test

import (
	"errors"
	"strings"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/onsi/gomega/format"

	boshlog "github.com/cloudfoundry/bosh-utils/logger"
	fakesys "github.com/cloudfoundry/bosh-utils/system/fakes"

	"github.com/cloudfoundry/bosh-agent/v2/factory"
	. "github.com/cloudfoundry/bosh-agent/v2/platform/net"
	fakearp "github.com/cloudfoundry/bosh-agent/v2/platform/net/arp/fakes"
	fakednsresolver "github.com/cloudfoundry/bosh-agent/v2/platform/net/dnsresolver/fakes"
	fakenet "github.com/cloudfoundry/bosh-agent/v2/platform/net/fakes"
	boship "github.com/cloudfoundry/bosh-agent/v2/platform/net/ip"
	fakeip "github.com/cloudfoundry/bosh-agent/v2/platform/net/ip/fakes"
	"github.com/cloudfoundry/bosh-agent/v2/platform/net/netfakes"
	boshsettings "github.com/cloudfoundry/bosh-agent/v2/settings"
)

var _ = Describe("ubuntuNetManager", func() {
	var (
		fs                            *fakesys.FakeFileSystem
		cmdRunner                     *fakesys.FakeCmdRunner
		ipResolver                    *fakeip.FakeResolver
		addressBroadcaster            *fakearp.FakeAddressBroadcaster
		interfaceAddrsProvider        *fakeip.FakeInterfaceAddressesProvider
		kernelIPv6                    *fakenet.FakeKernelIPv6
		netManager                    UbuntuNetManager
		interfaceConfigurationCreator InterfaceConfigurationCreator
		fakeMACAddressDetector        *netfakes.FakeMACAddressDetector
	)

	stubInterfaces := func(physicalInterfaces map[string]boshsettings.Network) {
		addresses := map[string]string{}
		for iface, networkSettings := range physicalInterfaces {
			addresses[networkSettings.Mac] = iface
		}

		fakeMACAddressDetector.DetectMacAddressesReturns(addresses, nil)
	}

	BeforeEach(func() {
		fs = fakesys.NewFakeFileSystem()
		cmdRunner = fakesys.NewFakeCmdRunner()
		ipResolver = &fakeip.FakeResolver{}
		logger := boshlog.NewLogger(boshlog.LevelNone)
		fakeMACAddressDetector = &netfakes.FakeMACAddressDetector{}
		interfaceConfigurationCreator = NewInterfaceConfigurationCreator(logger)
		addressBroadcaster = &fakearp.FakeAddressBroadcaster{}
		interfaceAddrsProvider = &fakeip.FakeInterfaceAddressesProvider{}
		fakeDnsResolver := &fakednsresolver.FakeDNSResolver{}
		kernelIPv6 = &fakenet.FakeKernelIPv6{}
		netManager = NewUbuntuNetManager(
			fs,
			cmdRunner,
			ipResolver,
			fakeMACAddressDetector,
			interfaceConfigurationCreator,
			interfaceAddrsProvider,
			fakeDnsResolver,
			addressBroadcaster,
			kernelIPv6,
			logger,
		).(UbuntuNetManager)

		format.TruncatedDiff = false
	})

	Describe("ComputeNetworkConfig", func() {
		Context("when there is one manual network and neither is marked as default for DNS", func() {
			It("should use the manual network for DNS", func() {
				networks := boshsettings.Networks{
					"manual": factory.Network{DNS: &[]string{"8.8.8.8"}}.Build(),
				}
				stubInterfaces(networks)
				_, _, dnsServers, err := netManager.ComputeNetworkConfig(networks)
				Expect(err).ToNot(HaveOccurred())
				Expect(dnsServers).To(Equal([]string{"8.8.8.8"}))
			})
		})

		Context("when there is a vip network and a manual network and neither is marked as default for DNS", func() {
			It("should use the manual network for DNS", func() {
				networks := boshsettings.Networks{
					"vip":    boshsettings.Network{Type: "vip"},
					"manual": factory.Network{Type: "manual", DNS: &[]string{"8.8.8.8"}}.Build(),
				}
				stubInterfaces(networks)
				_, _, dnsServers, err := netManager.ComputeNetworkConfig(networks)
				Expect(err).ToNot(HaveOccurred())
				Expect(dnsServers).To(Equal([]string{"8.8.8.8"}))
			})
		})
		Context("when there is a vip network and a manual network and the manual network is marked as default for DNS", func() {
			It("should use the manual network for DNS", func() {
				networks := boshsettings.Networks{
					"vip":    boshsettings.Network{Type: "vip"},
					"manual": factory.Network{Type: "manual", DNS: &[]string{"8.8.8.8"}, Default: []string{"dns"}}.Build(),
				}
				stubInterfaces(networks)
				_, _, dnsServers, err := netManager.ComputeNetworkConfig(networks)
				Expect(err).ToNot(HaveOccurred())
				Expect(dnsServers).To(Equal([]string{"8.8.8.8"}))
			})
		})

		Context("when specified more than one DNS", func() {
			It("extracts all DNS servers from the network configured as default DNS", func() {
				networks := boshsettings.Networks{
					"default": factory.Network{
						IP:      "10.10.0.32",
						Netmask: "255.255.255.0",
						Mac:     "aa::bb::cc",
						Default: []string{"dns", "gateway"},
						DNS:     &[]string{"54.209.78.6", "127.0.0.5"},
						Gateway: "10.10.0.1",
					}.Build(),
				}
				stubInterfaces(networks)
				staticInterfaceConfigurations, dhcpInterfaceConfigurations, dnsServers, err := netManager.ComputeNetworkConfig(networks)
				Expect(err).ToNot(HaveOccurred())

				Expect(staticInterfaceConfigurations).To(Equal([]StaticInterfaceConfiguration{
					{
						Name:                "default",
						Address:             "10.10.0.32",
						Netmask:             "255.255.255.0",
						Network:             "10.10.0.0",
						IsDefaultForGateway: true,
						Broadcast:           "10.10.0.255",
						Mac:                 "aa::bb::cc",
						Gateway:             "10.10.0.1",
					},
				}))
				Expect(dhcpInterfaceConfigurations).To(BeEmpty())
				Expect(dnsServers).To(Equal([]string{"54.209.78.6", "127.0.0.5"}))
			})
		})

		Context("when interface alias exists in network settings", func() {
			It("static interface configuration should be constructed by alias name", func() {
				networks := boshsettings.Networks{
					"default": factory.Network{
						IP:      "10.10.0.32",
						Netmask: "255.255.255.0",
						Mac:     "aa::bb::cc",
						Default: []string{"dns", "gateway"},
						DNS:     &[]string{"54.209.78.6", "127.0.0.5"},
						Gateway: "10.10.0.1",
						Alias:   "eth0:0",
					}.Build(),
				}
				stubInterfaces(networks)
				staticInterfaceConfigurations, dhcpInterfaceConfigurations, dnsServers, err := netManager.ComputeNetworkConfig(networks)
				Expect(err).ToNot(HaveOccurred())

				Expect(staticInterfaceConfigurations).To(Equal([]StaticInterfaceConfiguration{
					{
						Name:                "default",
						Address:             "10.10.0.32",
						Netmask:             "255.255.255.0",
						Network:             "10.10.0.0",
						IsDefaultForGateway: true,
						Broadcast:           "10.10.0.255",
						Mac:                 "aa::bb::cc",
						Gateway:             "10.10.0.1",
					},
				}))
				Expect(dhcpInterfaceConfigurations).To(BeEmpty())
				Expect(dnsServers).To(Equal([]string{"54.209.78.6", "127.0.0.5"}))
			})

			It("collapses virtual interface into network bound by MAC address", func() {
				networks := boshsettings.Networks{
					"default": factory.Network{
						IP:      "10.10.0.32",
						Netmask: "255.255.255.0",
						Mac:     "mac-1",
						Default: []string{"dns", "gateway"},
						DNS:     &[]string{"54.209.78.6", "127.0.0.5"},
						Gateway: "10.10.0.1",
					}.Build(),
					"second": factory.Network{
						IP:      "10.10.0.33",
						Netmask: "255.255.255.0",
						Default: []string{"dns", "gateway"},
						DNS:     &[]string{"54.209.78.6", "127.0.0.5"},
						Gateway: "10.10.0.1",
						Alias:   "eth0:1",
					}.Build(),
					"third": factory.Network{
						IP:      "10.10.0.34",
						Netmask: "255.255.255.0",
						Default: []string{"dns", "gateway"},
						DNS:     &[]string{"54.209.78.6", "127.0.0.5"},
						Gateway: "10.10.0.1",
						Alias:   "eth0:2",
					}.Build(),
				}

				addresses := map[string]string{}
				addresses["mac-1"] = "eth0"

				fakeMACAddressDetector.DetectMacAddressesReturns(addresses, nil)
				staticInterfaceConfigurations, dhcpInterfaceConfigurations, dnsServers, err := netManager.ComputeNetworkConfig(networks)
				Expect(err).ToNot(HaveOccurred())

				Expect(len(staticInterfaceConfigurations)).To(Equal(1))
				Expect(staticInterfaceConfigurations[0].Name).To(Equal("eth0"))
				Expect(staticInterfaceConfigurations[0].Address).To(Equal("10.10.0.32"))
				Expect(staticInterfaceConfigurations[0].Netmask).To(Equal("255.255.255.0"))
				Expect(staticInterfaceConfigurations[0].Network).To(Equal("10.10.0.0"))
				Expect(staticInterfaceConfigurations[0].IsDefaultForGateway).To(Equal(true))
				Expect(staticInterfaceConfigurations[0].Broadcast).To(Equal("10.10.0.255"))
				Expect(staticInterfaceConfigurations[0].Mac).To(Equal("mac-1"))
				Expect(staticInterfaceConfigurations[0].Gateway).To(Equal("10.10.0.1"))
				Expect(staticInterfaceConfigurations[0].VirtualInterfaces).To(ConsistOf([]VirtualInterface{
					VirtualInterface{Label: "eth0:1", Address: "10.10.0.33/24"},
					VirtualInterface{Label: "eth0:2", Address: "10.10.0.34/24"},
				}))
				Expect(dhcpInterfaceConfigurations).To(BeEmpty())
				Expect(dnsServers).To(Equal([]string{"54.209.78.6", "127.0.0.5"}))
			})
		})
	})

	Describe("SetupNetworking", func() {
		var (
			dhcpNetwork                                  boshsettings.Network
			staticNetwork                                boshsettings.Network
			expectedNetworkConfigurationForStaticAndDhcp string
		)

		BeforeEach(func() {
			dhcpNetwork = boshsettings.Network{
				Type:    "dynamic",
				Default: []string{"dns"},
				DNS:     []string{"8.8.8.8", "9.9.9.9"},
				Mac:     "fake-dhcp-mac-address",
			}
			staticNetwork = boshsettings.Network{
				Type:    "manual",
				IP:      "1.2.3.4",
				Default: []string{"gateway"},
				Netmask: "255.255.255.0",
				Gateway: "3.4.5.6",
				Mac:     "fake-static-mac-address",
			}
			interfaceAddrsProvider.GetInterfaceAddresses = []boship.InterfaceAddress{
				boship.NewSimpleInterfaceAddress("ethstatic", "1.2.3.4"),
			}
			expectedNetworkConfigurationForStaticAndDhcp = `# Generated by bosh-agent
auto lo
iface lo inet loopback

auto ethdhcp
iface ethdhcp inet dhcp

auto ethstatic
iface ethstatic inet static
    address 1.2.3.4
    network 1.2.3.0
    netmask 255.255.255.0
    broadcast 1.2.3.255
    gateway 3.4.5.6

dns-nameservers 8.8.8.8 9.9.9.9`
		})

		Context("networks is preconfigured", func() {
			var networks boshsettings.Networks
			BeforeEach(func() {
				dhcpNetwork.Preconfigured = true
				staticNetwork.Preconfigured = true
				networks = boshsettings.Networks{
					"first":  dhcpNetwork,
					"second": staticNetwork,
				}

				Expect(networks.IsPreconfigured()).To(BeTrue())
			})

		})

		It("configures gateway, broadcast and dns for default network only", func() {
			staticNetwork = boshsettings.Network{
				Type:    "manual",
				IP:      "1.2.3.4",
				Netmask: "255.255.255.0",
				Gateway: "3.4.5.6",
				Mac:     "fake-static-mac-address",
			}
			secondStaticNetwork := boshsettings.Network{
				Type:    "manual",
				IP:      "5.6.7.8",
				Netmask: "255.255.255.0",
				Gateway: "6.7.8.9",
				Mac:     "second-fake-static-mac-address",
				DNS:     []string{"8.8.8.8"},
				Default: []string{"gateway", "dns"},
			}

			stubInterfaces(map[string]boshsettings.Network{
				"eth0": staticNetwork,
				"eth1": secondStaticNetwork,
			})

			interfaceAddrsProvider.GetInterfaceAddresses = []boship.InterfaceAddress{
				boship.NewSimpleInterfaceAddress("eth0", "1.2.3.4"),
				boship.NewSimpleInterfaceAddress("eth1", "5.6.7.8"),
			}

			err := netManager.SetupNetworking(boshsettings.Networks{
				"static-1": staticNetwork,
				"static-2": secondStaticNetwork,
			}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			matches, err := fs.Ls("/etc/systemd/network/")
			Expect(err).NotTo(HaveOccurred())
			Expect(matches).To(ConsistOf(
				"/etc/systemd/network/10_eth0.network",
				"/etc/systemd/network/10_eth1.network",
			))
			networkConfig := fs.GetFileTestStat("/etc/systemd/network/10_eth0.network")
			Expect(networkConfig).ToNot(BeNil())
			Expect(networkConfig.StringContents()).To(Equal(`# Generated by bosh-agent
[Match]
Name=eth0

[Address]
Address=1.2.3.4/24

[Network]
DNS=8.8.8.8

`))
			networkConfig = fs.GetFileTestStat("/etc/systemd/network/10_eth1.network")
			Expect(networkConfig).ToNot(BeNil())
			Expect(networkConfig.StringContents()).To(Equal(`# Generated by bosh-agent
[Match]
Name=eth1

[Address]
Address=5.6.7.8/24
Broadcast=5.6.7.255

[Network]
Gateway=6.7.8.9
DNS=8.8.8.8

`))
		})

		It("ensures the only interfaces configured are the ones currently configured when SetupNetworking is re-run", func() {
			By("Pre-configuring the network to have two devices", func() {
				staticNetwork = boshsettings.Network{
					Type:    "manual",
					IP:      "1.2.3.4",
					Netmask: "255.255.255.0",
					Gateway: "3.4.5.6",
					Mac:     "fake-static-mac-address",
				}
				secondStaticNetwork := boshsettings.Network{
					Type:    "manual",
					IP:      "5.6.7.8",
					Netmask: "255.255.255.0",
					Gateway: "6.7.8.9",
					Mac:     "second-fake-static-mac-address",
					DNS:     []string{"8.8.8.8"},
					Default: []string{"gateway", "dns"},
				}

				stubInterfaces(map[string]boshsettings.Network{
					"eth0": staticNetwork,
					"eth1": secondStaticNetwork,
				})

				interfaceAddrsProvider.GetInterfaceAddresses = []boship.InterfaceAddress{
					boship.NewSimpleInterfaceAddress("eth0", "1.2.3.4"),
					boship.NewSimpleInterfaceAddress("eth1", "5.6.7.8"),
				}

				err := netManager.SetupNetworking(boshsettings.Networks{
					"static-1": staticNetwork,
					"static-2": secondStaticNetwork,
				}, "", nil)
				Expect(err).ToNot(HaveOccurred())

				matches, err := fs.Ls("/etc/systemd/network/")
				Expect(err).NotTo(HaveOccurred())
				Expect(matches).To(ConsistOf(
					"/etc/systemd/network/10_eth0.network",
					"/etc/systemd/network/10_eth1.network",
				))
				networkConfig := fs.GetFileTestStat("/etc/systemd/network/10_eth0.network")
				Expect(networkConfig).ToNot(BeNil())
				networkConfig = fs.GetFileTestStat("/etc/systemd/network/10_eth1.network")
				Expect(networkConfig).ToNot(BeNil())
			})

			By("Reconfiguring to only have one", func() {
				// Otherwise, running SetupNetworking twice with different networks could leak networks
				staticNetwork = boshsettings.Network{
					Type:    "manual",
					IP:      "5.6.7.8",
					Netmask: "255.255.255.0",
					Gateway: "6.7.8.9",
					Mac:     "second-fake-static-mac-address",
					DNS:     []string{"8.8.8.8"},
					Default: []string{"gateway", "dns"},
				}

				stubInterfaces(map[string]boshsettings.Network{
					"eth0": staticNetwork,
				})

				interfaceAddrsProvider.GetInterfaceAddresses = []boship.InterfaceAddress{
					boship.NewSimpleInterfaceAddress("eth0", "5.6.7.8"),
				}

				err := netManager.SetupNetworking(boshsettings.Networks{
					"static-1": staticNetwork,
				}, "", nil)
				Expect(err).ToNot(HaveOccurred())

				matches, err := fs.Ls("/etc/systemd/network/")
				Expect(err).NotTo(HaveOccurred())
				Expect(matches).To(ConsistOf(
					"/etc/systemd/network/10_eth0.network",
				))
				networkConfig := fs.GetFileTestStat("/etc/systemd/network/10_eth0.network")
				Expect(networkConfig).ToNot(BeNil())
				Expect(networkConfig.StringContents()).To(Equal(`# Generated by bosh-agent
[Match]
Name=eth0

[Address]
Address=5.6.7.8/24
Broadcast=5.6.7.255

[Network]
Gateway=6.7.8.9
DNS=8.8.8.8

`))
			})
		})

		It("configures postup routes for static network", func() {
			staticNetwork = boshsettings.Network{
				Type:    "manual",
				IP:      "1.2.3.4",
				Netmask: "255.255.255.0",
				Gateway: "3.4.5.6",
				Mac:     "fake-static-mac-address",
				Routes: []boshsettings.Route{
					boshsettings.Route{
						Destination: "10.0.0.0",
						Gateway:     "3.4.5.6",
						Netmask:     "255.0.0.0",
					},
					boshsettings.Route{
						Destination: "10.0.1.0",
						Gateway:     "3.4.5.6",
						Netmask:     "255.0.0.0",
					},
				},
			}
			secondStaticNetwork := boshsettings.Network{
				Type:    "manual",
				IP:      "5.6.7.8",
				Netmask: "255.255.255.0",
				Gateway: "6.7.8.9",
				Mac:     "second-fake-static-mac-address",
				DNS:     []string{"8.8.8.8"},
				Default: []string{"gateway", "dns"},
			}

			stubInterfaces(map[string]boshsettings.Network{
				"eth0": staticNetwork,
				"eth1": secondStaticNetwork,
			})

			interfaceAddrsProvider.GetInterfaceAddresses = []boship.InterfaceAddress{
				boship.NewSimpleInterfaceAddress("eth0", "1.2.3.4"),
				boship.NewSimpleInterfaceAddress("eth1", "5.6.7.8"),
			}

			err := netManager.SetupNetworking(boshsettings.Networks{
				"static-1": staticNetwork,
				"static-2": secondStaticNetwork,
			}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			matches, err := fs.Ls("/etc/systemd/network/")
			Expect(err).ToNot(HaveOccurred())
			Expect(matches).To(ConsistOf(
				"/etc/systemd/network/10_eth0.network",
				"/etc/systemd/network/10_eth1.network",
			))
			networkConfig := fs.GetFileTestStat("/etc/systemd/network/10_eth0.network")
			Expect(networkConfig).ToNot(BeNil())
			Expect(networkConfig.StringContents()).To(Equal(`# Generated by bosh-agent
[Match]
Name=eth0

[Address]
Address=1.2.3.4/24

[Network]
DNS=8.8.8.8

[Route]
Destination=10.0.0.0/8
Gateway=3.4.5.6

[Route]
Destination=10.0.1.0/8
Gateway=3.4.5.6

`))
			networkConfig = fs.GetFileTestStat("/etc/systemd/network/10_eth1.network")
			Expect(networkConfig).ToNot(BeNil())
			Expect(networkConfig.StringContents()).To(Equal(`# Generated by bosh-agent
[Match]
Name=eth1

[Address]
Address=5.6.7.8/24
Broadcast=5.6.7.255

[Network]
Gateway=6.7.8.9
DNS=8.8.8.8

`))
		})

		It("configures postup routes for dynamic network", func() {
			staticNetwork = boshsettings.Network{
				Type:    "dynamic",
				IP:      "1.2.3.4",
				Netmask: "255.255.255.0",
				Gateway: "3.4.5.6",
				DNS:     []string{"8.8.8.8"},
				Mac:     "fake-static-mac-address",
				Routes: []boshsettings.Route{
					boshsettings.Route{
						Destination: "10.0.0.0",
						Gateway:     "3.4.5.6",
						Netmask:     "255.0.0.0",
					},
					boshsettings.Route{
						Destination: "10.0.1.0",
						Gateway:     "3.4.5.6",
						Netmask:     "255.0.0.0",
					},
				},
			}

			stubInterfaces(map[string]boshsettings.Network{
				"eth0": staticNetwork,
			})

			interfaceAddrsProvider.GetInterfaceAddresses = []boship.InterfaceAddress{
				boship.NewSimpleInterfaceAddress("eth0", "1.2.3.4"),
				boship.NewSimpleInterfaceAddress("eth1", "5.6.7.8"),
			}

			err := netManager.SetupNetworking(boshsettings.Networks{
				"static-1": staticNetwork,
			}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			matches, err := fs.Ls("/etc/systemd/network/")
			Expect(err).ToNot(HaveOccurred())
			Expect(matches).To(ConsistOf(
				"/etc/systemd/network/10_eth0.network",
			))
			networkConfig := fs.GetFileTestStat("/etc/systemd/network/10_eth0.network")
			Expect(networkConfig).ToNot(BeNil())
			Expect(networkConfig.StringContents()).To(Equal(`# Generated by bosh-agent
[Match]
Name=eth0

[Network]
DHCP=yes
DNS=8.8.8.8

[DHCP]
UseDomains=yes
UseMTU=yes

[Route]
Destination=10.0.0.0/8
Gateway=3.4.5.6

[Route]
Destination=10.0.1.0/8
Gateway=3.4.5.6

`))
		})

		It("writes /etc/network/interfaces without dns-namservers if there are no dns servers", func() {
			staticNetworkWithoutDNS := boshsettings.Network{
				Type:    "manual",
				IP:      "1.2.3.4",
				Default: []string{"gateway"},
				Netmask: "255.255.255.0",
				Gateway: "3.4.5.6",
				Mac:     "fake-static-mac-address",
			}

			stubInterfaces(map[string]boshsettings.Network{
				"ethstatic": staticNetworkWithoutDNS,
			})

			err := netManager.SetupNetworking(boshsettings.Networks{"static-network": staticNetworkWithoutDNS}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			matches, err := fs.Ls("/etc/systemd/network/")
			Expect(err).ToNot(HaveOccurred())
			Expect(matches).To(ConsistOf("/etc/systemd/network/10_ethstatic.network"))

			networkConfig := fs.GetFileTestStat("/etc/systemd/network/10_ethstatic.network")
			Expect(networkConfig).ToNot(BeNil())
			Expect(networkConfig.StringContents()).To(Equal(`# Generated by bosh-agent
[Match]
Name=ethstatic

[Address]
Address=1.2.3.4/24
Broadcast=1.2.3.255

[Network]
Gateway=3.4.5.6

`))
		})

		It("returns errors from writing the network configuration", func() {
			stubInterfaces(map[string]boshsettings.Network{
				"dhcp":   dhcpNetwork,
				"static": staticNetwork,
			})
			fs.WriteFileError = errors.New("fs-write-file-error")
			err := netManager.SetupNetworking(boshsettings.Networks{"dhcp-network": dhcpNetwork, "static-network": staticNetwork}, "", nil)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("fs-write-file-error"))
		})

		It("returns errors when it can't creating network interface configurations", func() {
			stubInterfaces(map[string]boshsettings.Network{
				"ethdhcp":   dhcpNetwork,
				"ethstatic": staticNetwork,
			})
			staticNetwork.Netmask = "not an ip" // will cause InterfaceConfigurationCreator to fail
			err := netManager.SetupNetworking(boshsettings.Networks{"static-network": staticNetwork}, "", nil)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("Creating interface configurations"))
		})

		It("returns errors when there a netmask cannot be converted to a CIDR", func() {
			staticNetwork = boshsettings.Network{
				Type:    "manual",
				IP:      "1.2.3.4",
				Netmask: "255.0.255.0",
				Gateway: "3.4.5.6",
				Mac:     "fake-static-mac-address",
			}

			stubInterfaces(map[string]boshsettings.Network{
				"eth0": staticNetwork,
			})

			interfaceAddrsProvider.GetInterfaceAddresses = []boship.InterfaceAddress{
				boship.NewSimpleInterfaceAddress("eth0", "1.2.3.4"),
			}

			err := netManager.SetupNetworking(boshsettings.Networks{
				"static-1": staticNetwork,
			}, "", nil)
			Expect(err).To(HaveOccurred())
			Expect(err).To(MatchError(`Updating network configs: Writing network configuration: Updating network configuration for eth0: netmask cannot be converted to CIDR: 255.0.255.0`))
		})

		It("writes a dhcp configuration if there are dhcp networks", func() {
			stubInterfaces(map[string]boshsettings.Network{
				"ethdhcp":   dhcpNetwork,
				"ethstatic": staticNetwork,
			})

			err := netManager.SetupNetworking(boshsettings.Networks{"dhcp-network": dhcpNetwork, "static-network": staticNetwork}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			dhcpConfig := fs.GetFileTestStat("/etc/dhcp/dhclient.conf")
			Expect(dhcpConfig).ToNot(BeNil())
			Expect(dhcpConfig.StringContents()).To(Equal(`# Generated by bosh-agent

option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;

send host-name = gethostname();

request subnet-mask, broadcast-address, time-offset, routers,
	domain-name, domain-name-servers, domain-search, host-name,
	netbios-name-servers, netbios-scope, interface-mtu,
	rfc3442-classless-static-routes, ntp-servers;

prepend domain-name-servers 8.8.8.8, 9.9.9.9;
`))

		})

		It("writes a dhcp configuration without prepended dns servers if there are no dns servers specified", func() {
			dhcpNetworkWithoutDNS := boshsettings.Network{
				Type: "dynamic",
				Mac:  "fake-dhcp-mac-address",
			}

			stubInterfaces(map[string]boshsettings.Network{
				"ethdhcp": dhcpNetwork,
			})

			err := netManager.SetupNetworking(boshsettings.Networks{"dhcp-network": dhcpNetworkWithoutDNS}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			dhcpConfig := fs.GetFileTestStat("/etc/dhcp/dhclient.conf")
			Expect(dhcpConfig).ToNot(BeNil())
			Expect(dhcpConfig.StringContents()).To(Equal(`# Generated by bosh-agent

option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;

send host-name = gethostname();

request subnet-mask, broadcast-address, time-offset, routers,
	domain-name, domain-name-servers, domain-search, host-name,
	netbios-name-servers, netbios-scope, interface-mtu,
	rfc3442-classless-static-routes, ntp-servers;

`))

		})

		It("returns an error if it can't write a dhcp configuration", func() {
			stubInterfaces(map[string]boshsettings.Network{
				"ethdhcp":   dhcpNetwork,
				"ethstatic": staticNetwork,
			})

			fs.WriteFileErrors["/etc/dhcp/dhclient.conf"] = errors.New("dhclient.conf-write-error")

			err := netManager.SetupNetworking(boshsettings.Networks{"dhcp-network": dhcpNetwork, "static-network": staticNetwork}, "", nil)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("dhclient.conf-write-error"))
		})

		It("doesn't write a dhcp configuration if there are no dhcp networks", func() {
			stubInterfaces(map[string]boshsettings.Network{
				"ethstatic": staticNetwork,
			})

			err := netManager.SetupNetworking(boshsettings.Networks{"static-network": staticNetwork}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			dhcpConfig := fs.GetFileTestStat("/etc/dhcp/dhclient.conf")
			Expect(dhcpConfig).To(BeNil())
		})

		It("restarts the networks only if config changes", func() {
			initialDhcpConfig := `# Generated by bosh-agent

option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;

send host-name = gethostname();

request subnet-mask, broadcast-address, time-offset, routers,
	domain-name, domain-name-servers, domain-search, host-name,
	netbios-name-servers, netbios-scope, interface-mtu,
	rfc3442-classless-static-routes, ntp-servers;

prepend domain-name-servers 8.8.8.8, 9.9.9.9;
`

			stubInterfaces(map[string]boshsettings.Network{
				"ethdhcp":   dhcpNetwork,
				"ethstatic": staticNetwork,
			})

			err := fs.WriteFileString("/etc/dhcp/dhclient.conf", initialDhcpConfig)
			Expect(err).NotTo(HaveOccurred())

			// check that config files change after stop and before start
			cmdRunner.SetCmdCallback("ip --force link set ethdhcp down", func() {
				Expect(fs.ReadFileString("/etc/dhcp/dhclient.conf")).To(Equal(initialDhcpConfig))
			})

			cmdRunner.SetCmdCallback("ip --force link set ethstatic down", func() {
				Expect(fs.ReadFileString("/etc/dhcp/dhclient.conf")).To(Equal(initialDhcpConfig))
			})

			cmdRunner.SetCmdCallback("ip --force link up ethdhcp", func() {
				Expect(fs.ReadFileString("/etc/dhcp/dhclient.conf")).To(Equal(initialDhcpConfig))
			})
			cmdRunner.SetCmdCallback("ip --force link up ethstatic", func() {
				Expect(fs.ReadFileString("/etc/dhcp/dhclient.conf")).To(Equal(initialDhcpConfig))
			})

			err = netManager.SetupNetworking(boshsettings.Networks{"dhcp-network": dhcpNetwork, "static-network": staticNetwork}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			Expect(len(cmdRunner.RunCommands)).To(Equal(4))
			Expect(cmdRunner.RunCommands[0]).To(Equal([]string{"pkill", "dhclient"}))
			Expect(cmdRunner.RunCommands[1:3]).To(ContainElement([]string{"resolvconf", "-d", "ethdhcp.dhclient"}))
			Expect(cmdRunner.RunCommands[1:3]).To(ContainElement([]string{"resolvconf", "-d", "ethstatic.dhclient"}))
			Expect(cmdRunner.RunCommands[3]).To(Equal([]string{"/var/vcap/bosh/bin/restart_networking"}))

			Expect(fs.ReadFileString("/etc/dhcp/dhclient.conf")).To(Equal(initialDhcpConfig))

			By("Setting up the networks with the same inputs, we do not restart networking")
			cmdRunner.ClearCommandHistory()
			err = netManager.SetupNetworking(boshsettings.Networks{"dhcp-network": dhcpNetwork, "static-network": staticNetwork}, "", nil)
			Expect(err).ToNot(HaveOccurred())
		})

		It("doesn't restart the networks if /etc/network/interfaces and /etc/dhcp/dhclient.conf don't change", func() {
			initialDhcpConfig := `# Generated by bosh-agent

option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;

send host-name = gethostname();

request subnet-mask, broadcast-address, time-offset, routers,
	domain-name, domain-name-servers, domain-search, host-name,
	netbios-name-servers, netbios-scope, interface-mtu,
	rfc3442-classless-static-routes, ntp-servers;

prepend domain-name-servers 8.8.8.8, 9.9.9.9;
`
			stubInterfaces(map[string]boshsettings.Network{
				"ethdhcp":   dhcpNetwork,
				"ethstatic": staticNetwork,
			})

			err := fs.WriteFileString("/etc/network/interfaces", expectedNetworkConfigurationForStaticAndDhcp)
			Expect(err).NotTo(HaveOccurred())
			err = fs.WriteFileString("/etc/dhcp/dhclient.conf", initialDhcpConfig)
			Expect(err).NotTo(HaveOccurred())

			err = netManager.SetupNetworking(boshsettings.Networks{"dhcp-network": dhcpNetwork, "static-network": staticNetwork}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			networkConfig := fs.GetFileTestStat("/etc/network/interfaces")
			Expect(networkConfig.StringContents()).To(Equal(expectedNetworkConfigurationForStaticAndDhcp))
			dhcpConfig := fs.GetFileTestStat("/etc/dhcp/dhclient.conf")
			Expect(dhcpConfig.StringContents()).To(Equal(initialDhcpConfig))

			Expect(len(cmdRunner.RunCommands)).To(Equal(4))
			Expect(cmdRunner.RunCommands[0]).To(Equal([]string{"pkill", "dhclient"}))
			Expect(cmdRunner.RunCommands[1:3]).To(ContainElement([]string{"resolvconf", "-d", "ethdhcp.dhclient"}))
			Expect(cmdRunner.RunCommands[1:3]).To(ContainElement([]string{"resolvconf", "-d", "ethstatic.dhclient"}))
			Expect(cmdRunner.RunCommands[3]).To(Equal([]string{"/var/vcap/bosh/bin/restart_networking"}))
		})

		It("restarts the networks if /etc/dhcp/dhclient.conf changes", func() {
			initialDhcpConfig := "initial-dhcp-config"

			stubInterfaces(map[string]boshsettings.Network{
				"ethdhcp":   dhcpNetwork,
				"ethstatic": staticNetwork,
			})

			err := fs.WriteFileString("/etc/dhcp/dhclient.conf", initialDhcpConfig)
			Expect(err).NotTo(HaveOccurred())

			// check that config files change after stop and before start
			cmdRunner.SetCmdCallback("ip --force link set ethdhcp down", func() {
				Expect(fs.ReadFileString("/etc/dhcp/dhclient.conf")).To(Equal(initialDhcpConfig))
			})

			cmdRunner.SetCmdCallback("ip --force link set ethstatic down", func() {
				Expect(fs.ReadFileString("/etc/dhcp/dhclient.conf")).To(Equal(initialDhcpConfig))
			})

			cmdRunner.SetCmdCallback("ip --force link up ethdhcp", func() {
				Expect(fs.ReadFileString("/etc/dhcp/dhclient.conf")).To(Equal(initialDhcpConfig))
			})
			cmdRunner.SetCmdCallback("ip --force link up ethstatic", func() {
				Expect(fs.ReadFileString("/etc/dhcp/dhclient.conf")).To(Equal(initialDhcpConfig))
			})

			err = netManager.SetupNetworking(boshsettings.Networks{"dhcp-network": dhcpNetwork, "static-network": staticNetwork}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			Expect(len(cmdRunner.RunCommands)).To(Equal(4))
			Expect(cmdRunner.RunCommands[0]).To(Equal([]string{"pkill", "dhclient"}))
			Expect(cmdRunner.RunCommands[1:3]).To(ContainElement([]string{"resolvconf", "-d", "ethdhcp.dhclient"}))
			Expect(cmdRunner.RunCommands[1:3]).To(ContainElement([]string{"resolvconf", "-d", "ethstatic.dhclient"}))
			Expect(cmdRunner.RunCommands[3]).To(Equal([]string{"/var/vcap/bosh/bin/restart_networking"}))

			Expect(fs.ReadFileString("/etc/dhcp/dhclient.conf")).ToNot(Equal(initialDhcpConfig))
		})

		It("broadcasts MAC addresses for all interfaces", func() {
			stubInterfaces(map[string]boshsettings.Network{
				"ethdhcp":   dhcpNetwork,
				"ethstatic": staticNetwork,
			})

			err := netManager.SetupNetworking(boshsettings.Networks{"dhcp-network": dhcpNetwork, "static-network": staticNetwork}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			Eventually(func() []boship.InterfaceAddress { return addressBroadcaster.Value() }).Should(
				Equal([]boship.InterfaceAddress{
					boship.NewSimpleInterfaceAddress("ethstatic", "1.2.3.4"),
					boship.NewResolvingInterfaceAddress("ethdhcp", ipResolver),
				}),
			)
		})

		It("skips vip networks", func() {
			stubInterfaces(map[string]boshsettings.Network{
				"ethdhcp":   dhcpNetwork,
				"ethstatic": staticNetwork,
			})

			vipNetwork := boshsettings.Network{
				Type:    "vip",
				Default: []string{"dns"},
				DNS:     []string{"8.8.8.8", "9.9.9.9"},
				Mac:     "fake-vip-mac-address",
				IP:      "9.8.7.6",
			}

			err := netManager.SetupNetworking(boshsettings.Networks{
				"dhcp-network":   dhcpNetwork,
				"static-network": staticNetwork,
				"vip-network":    vipNetwork,
			}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			matches, err := fs.Ls("/etc/systemd/network/")
			Expect(err).NotTo(HaveOccurred())
			Expect(matches).To(ConsistOf(
				"/etc/systemd/network/10_ethdhcp.network",
				"/etc/systemd/network/10_ethstatic.network",
			))

			networkConfig := fs.GetFileTestStat("/etc/systemd/network/10_ethdhcp.network")
			Expect(networkConfig).ToNot(BeNil())
			Expect(networkConfig.StringContents()).To(Equal(`# Generated by bosh-agent
[Match]
Name=ethdhcp

[Network]
DHCP=yes
DNS=8.8.8.8
DNS=9.9.9.9

[DHCP]
UseDomains=yes
UseMTU=yes

`))

			networkConfig = fs.GetFileTestStat("/etc/systemd/network/10_ethstatic.network")
			Expect(networkConfig).ToNot(BeNil())
			Expect(networkConfig.StringContents()).To(Equal(`# Generated by bosh-agent
[Match]
Name=ethstatic

[Address]
Address=1.2.3.4/24
Broadcast=1.2.3.255

[Network]
Gateway=3.4.5.6
DNS=8.8.8.8
DNS=9.9.9.9

`))
		})

		Context("when manual networks were not configured with proper IP addresses", func() {
			BeforeEach(func() {
				interfaceAddrsProvider.GetInterfaceAddresses = []boship.InterfaceAddress{
					boship.NewSimpleInterfaceAddress("ethstatic", "1.2.3.5"),
				}
			})

			It("fails", func() {
				stubInterfaces(map[string]boshsettings.Network{
					"ethstatic": staticNetwork,
				})

				errCh := make(chan error)
				err := netManager.SetupNetworking(boshsettings.Networks{"static-network": staticNetwork}, "", errCh)
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(ContainSubstring("Validating static network configuration"))
			})
		})

		Context("when no MAC address is provided in the settings", func() {
			It("configures network for single device", func() {
				staticNetworkWithoutMAC := boshsettings.Network{
					Type:    "manual",
					IP:      "2.2.2.2",
					Default: []string{"gateway"},
					Netmask: "255.255.255.0",
					Gateway: "3.4.5.6",
				}

				stubInterfaces(
					map[string]boshsettings.Network{
						"ethstatic": staticNetwork,
					},
				)
				interfaceAddrsProvider.GetInterfaceAddresses = []boship.InterfaceAddress{
					boship.NewSimpleInterfaceAddress("ethstatic", "2.2.2.2"),
				}

				err := netManager.SetupNetworking(boshsettings.Networks{
					"static-network": staticNetworkWithoutMAC,
				}, "", nil)
				Expect(err).ToNot(HaveOccurred())

				matches, err := fs.Ls("/etc/systemd/network/")
				Expect(err).NotTo(HaveOccurred())
				Expect(matches).To(ConsistOf(
					"/etc/systemd/network/10_ethstatic.network",
				))

				networkConfig := fs.GetFileTestStat("/etc/systemd/network/10_ethstatic.network")
				Expect(networkConfig).ToNot(BeNil())
				Expect(networkConfig.StringContents()).To(Equal(`# Generated by bosh-agent
[Match]
Name=ethstatic

[Address]
Address=2.2.2.2/24
Broadcast=2.2.2.255

[Network]
Gateway=3.4.5.6

`))
			})
		})

		Context("when manual networks were configured with portable IP", func() {
			var (
				portableNetwork boshsettings.Network
				staticNetwork   boshsettings.Network
				staticNetwork1  boshsettings.Network
			)
			BeforeEach(func() {
				portableNetwork = boshsettings.Network{
					Type:     "manual",
					IP:       "10.112.166.136",
					Netmask:  "255.255.255.192",
					Resolved: false,
					UseDHCP:  false,
					DNS:      []string{"8.8.8.8"},
					Alias:    "eth0:0",
				}
				staticNetwork = boshsettings.Network{
					Type:     "dynamic",
					IP:       "169.50.68.75",
					Netmask:  "255.255.255.224",
					Gateway:  "169.50.68.65",
					Default:  []string{"gateway", "dns"},
					Resolved: false,
					UseDHCP:  false,
					DNS:      []string{"8.8.8.8", "10.0.80.11", "10.0.80.12"},
					Mac:      "06:64:d4:7d:63:71",
					Alias:    "eth1",
				}
				staticNetwork1 = boshsettings.Network{
					Type:     "dynamic",
					IP:       "10.112.39.113",
					Netmask:  "255.255.255.128",
					Resolved: false,
					UseDHCP:  false,
					DNS:      []string{"8.8.8.8", "10.0.80.11", "10.0.80.12"},
					Mac:      "06:b7:e8:0c:38:d8",
					Alias:    "eth0",
				}
				interfaceAddrsProvider.GetInterfaceAddresses = []boship.InterfaceAddress{
					boship.NewSimpleInterfaceAddress("eth0", "10.112.39.113"),
					boship.NewSimpleInterfaceAddress("eth1", "169.50.68.75"),
				}
				err := fs.WriteFileString("/etc/resolv.conf", `
nameserver 8.8.8.8
nameserver 10.0.80.11
nameserver 10.0.80.12
`)
				Expect(err).NotTo(HaveOccurred())
			})

			scrubMultipleLines := func(in string) string {
				return strings.ReplaceAll(in, "\n\n\n", "\n\n")
			}

			It("succeeds", func() {
				stubInterfaces(map[string]boshsettings.Network{
					"eth1": staticNetwork,
					"eth0": staticNetwork1,
				})

				err := netManager.SetupNetworking(boshsettings.Networks{"default": portableNetwork, "dynamic": staticNetwork, "dynamic_1": staticNetwork1}, "", nil)
				Expect(err).NotTo(HaveOccurred())

				Eventually(func() []boship.InterfaceAddress { return addressBroadcaster.Value() }).Should(
					Equal([]boship.InterfaceAddress{
						boship.NewSimpleInterfaceAddress("eth0", "10.112.39.113"),
						boship.NewSimpleInterfaceAddress("eth1", "169.50.68.75"),
					}),
				)

				matches, err := fs.Ls("/etc/systemd/network/")
				Expect(err).NotTo(HaveOccurred())
				Expect(matches).To(ConsistOf(
					"/etc/systemd/network/10_eth0.network",
					"/etc/systemd/network/10_eth1.network",
				))

				format.TruncatedDiff = false

				networkConfig := fs.GetFileTestStat("/etc/systemd/network/10_eth0.network")
				Expect(networkConfig).ToNot(BeNil())
				Expect(networkConfig.StringContents()).To(Equal(`# Generated by bosh-agent
[Match]
Name=eth0

[Address]
Address=10.112.39.113/25

[Address]
Label=eth0:0
Address=10.112.166.136/26

[Network]
DNS=8.8.8.8
DNS=10.0.80.11
DNS=10.0.80.12

`))
				networkConfig = fs.GetFileTestStat("/etc/systemd/network/10_eth1.network")
				Expect(networkConfig).ToNot(BeNil())
				Expect(scrubMultipleLines(networkConfig.StringContents())).To(Equal(`# Generated by bosh-agent
[Match]
Name=eth1

[Address]
Address=169.50.68.75/27
Broadcast=169.50.68.95

[Network]
Gateway=169.50.68.65
DNS=8.8.8.8
DNS=10.0.80.11
DNS=10.0.80.12

`))
			})
		})
	})

	Describe("GetConfiguredNetworkInterfaces", func() {
		Context("when there are network devices", func() {
			BeforeEach(func() {
				stubInterfaces(map[string]boshsettings.Network{
					"fake-eth0": boshsettings.Network{Mac: "aa:bb"},
					"fake-eth1": boshsettings.Network{Mac: "cc:dd"},
					"fake-eth2": boshsettings.Network{Mac: "ee:ff"},
					"fake-eth3": boshsettings.Network{Mac: "yy:zz"},
				})
			})

			It("returns networks that are defined in /etc/network/interfaces", func() {
				err := fs.WriteFileString("/etc/systemd/network/10_fake-eth0.network", ``)
				Expect(err).NotTo(HaveOccurred())
				err = fs.WriteFileString("/etc/systemd/network/10_fake-eth2.network", ``)
				Expect(err).NotTo(HaveOccurred())

				interfaces, err := netManager.GetConfiguredNetworkInterfaces()
				Expect(err).ToNot(HaveOccurred())

				Expect(interfaces).To(ConsistOf("fake-eth0", "fake-eth2"))
			})
		})

		Context("when there are no network devices", func() {
			It("returns empty list", func() {
				interfaces, err := netManager.GetConfiguredNetworkInterfaces()
				Expect(err).ToNot(HaveOccurred())
				Expect(interfaces).To(Equal([]string{}))
			})
		})
	})

	Describe("SetupIPv6", func() {
		var (
			config boshsettings.IPv6
			stopCh chan struct{}
		)

		BeforeEach(func() {
			config = boshsettings.IPv6{}
			stopCh = make(chan struct{}, 1)
		})

		act := func() error { return netManager.SetupIPv6(config, stopCh) }

		Context("when IPv6 is enabled by the user", func() {
			It("enables IPv6", func() {
				config.Enable = true
				Expect(act()).ToNot(HaveOccurred())
				Expect(kernelIPv6.Enabled).To(BeTrue())
			})
		})

		Context("when IPv6 is NOT enabled by the user", func() {
			It("does not enable IPv6", func() {
				config.Enable = false
				Expect(act()).ToNot(HaveOccurred())
				Expect(kernelIPv6.Enabled).To(BeFalse())
			})
		})
	})
})
