diff --git a/vsphere/provider.go b/vsphere/provider.go index bbee8c475..d396c5f54 100644 --- a/vsphere/provider.go +++ b/vsphere/provider.go @@ -127,6 +127,7 @@ func Provider() terraform.ResourceProvider { "vsphere_vmfs_datastore": resourceVSphereVmfsDatastore(), "vsphere_virtual_machine_snapshot": resourceVSphereVirtualMachineSnapshot(), "vsphere_host": resourceVsphereHost(), + "vsphere_vnic": resourceVsphereNic(), }, DataSourcesMap: map[string]*schema.Resource{ diff --git a/vsphere/resource_vsphere_vnic.go b/vsphere/resource_vsphere_vnic.go new file mode 100644 index 000000000..4e2fd281d --- /dev/null +++ b/vsphere/resource_vsphere_vnic.go @@ -0,0 +1,568 @@ +package vsphere + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform/helper/schema" + "github.com/terraform-providers/terraform-provider-vsphere/vsphere/internal/helper/hostsystem" + "github.com/vmware/govmomi" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/types" + "log" + "strconv" + "strings" +) + +func resourceVsphereNic() *schema.Resource { + return &schema.Resource{ + Create: resourceVsphereNicCreate, + Read: resourceVsphereNicRead, + Update: resourceVsphereNicUpdate, + Delete: resourceVsphereNicDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: vNicSchema(), + } +} + +func vNicSchema() map[string]*schema.Schema { + base := BaseVMKernelSchema() + base["host"] = &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "ESX host the interface belongs to", + ForceNew: true, + } + + return base +} + +func resourceVsphereNicRead(d *schema.ResourceData, meta interface{}) error { + ctx := context.TODO() + client := meta.(*VSphereClient).vimClient + tfNicID := d.Id() + + toks := strings.Split(tfNicID, "_") + hostId := toks[0] + nicId := toks[1] + + vnic, err := getVnicFromHost(ctx, client, hostId, nicId) + if err != nil { + log.Printf("[DEBUG] Nic (%s) not found. Probably deleted.", nicId) + d.SetId("") + return nil + } + + _ = d.Set("portgroup", vnic.Portgroup) + if vnic.Spec.DistributedVirtualPort != nil { + _ = d.Set("distributed_switch_port", vnic.Spec.DistributedVirtualPort.SwitchUuid) + _ = d.Set("distributed_port_group", vnic.Spec.DistributedVirtualPort.PortgroupKey) + } + _ = d.Set("mtu", vnic.Spec.Mtu) + _ = d.Set("mac", vnic.Spec.Mac) + //_ = d.Set("netstack", vnic.Spec.NetStackInstanceKey) + + // Do we have any ipv4 config ? + if _, ok := d.GetOk("ipv4"); ok { + // if DHCP is true then we should ignore whatever addresses are set here. + ipv4dict := make(map[string]interface{}) + ipv4dict["dhcp"] = vnic.Spec.Ip.Dhcp + if !vnic.Spec.Ip.Dhcp { + ipv4dict["ip"] = vnic.Spec.Ip.IpAddress + ipv4dict["netmask"] = vnic.Spec.Ip.SubnetMask + if vnic.Spec.IpRouteSpec != nil { + ipv4dict["gw"] = vnic.Spec.IpRouteSpec.IpRouteConfig.GetHostIpRouteConfig().DefaultGateway + } + } + _ = d.Set("ipv4", []map[string]interface{}{ipv4dict}) + } + + // Do we have any ipv6 config ? + if _, ok := d.GetOk("ipv6"); ok { + ipv6dict := map[string]interface{}{ + "dhcp": *vnic.Spec.Ip.IpV6Config.DhcpV6Enabled, + "autoconfig": *vnic.Spec.Ip.IpV6Config.AutoConfigurationEnabled, + } + // First we need to filter out addresses that were configured via dhcp or autoconfig + // or link local or any other mechanism + addrList := make([]string, 0) + for _, addr := range vnic.Spec.Ip.IpV6Config.IpV6Address { + if addr.Origin == "manual" { + addrList = append(addrList, fmt.Sprintf("%s/%d", addr.IpAddress, addr.PrefixLength)) + } + } + ipv6dict["addresses"] = addrList + if vnic.Spec.IpRouteSpec != nil { + ipv6dict["gw"] = vnic.Spec.IpRouteSpec.IpRouteConfig.GetHostIpRouteConfig().IpV6DefaultGateway + } else if _, ok := d.GetOk("ipv6.0.gw"); ok { + // There is a gw set in the config, but none set on the Host. + ipv6dict["gw"] = "" + } + d.Set("ipv6", []map[string]interface{}{ipv6dict}) + } + return nil +} + +func resourceVsphereNicCreate(d *schema.ResourceData, meta interface{}) error { + nicId, err := createVNic(d, meta) + if err != nil { + return err + } + log.Printf("[DEBUG] Created NIC with ID: %s", nicId) + hostId := d.Get("host") + tfNicID := fmt.Sprintf("%s_%s", hostId, nicId) + d.SetId(tfNicID) + return resourceVsphereNicRead(d, meta) +} + +func resourceVsphereNicUpdate(d *schema.ResourceData, meta interface{}) error { + for _, k := range []string{ + "portgroup", "distributed_switch_port", "distributed_port_group", + "mac", "mtu", "ipv4", "ipv6", "netstack"} { + if d.HasChange(k) { + _, err := updateVNic(d, meta) + if err != nil { + return err + } + break + } + } + return resourceVsphereNicRead(d, meta) +} + +func resourceVsphereNicDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*VSphereClient).vimClient + idParts := strings.Split(d.Id(), "_") + hostId := idParts[0] + nicId := idParts[1] + + err := removeVnic(client, hostId, nicId) + if err != nil { + return err + } + return resourceVsphereNicRead(d, meta) +} + +// VmKernelSchema returns the schema required to represent a vNIC adapter on an ESX Host. +// We make this public so we can pull this from the host resource as well. +func BaseVMKernelSchema() map[string]*schema.Schema { + sch := map[string]*schema.Schema{ + "portgroup": { + Type: schema.TypeString, + Optional: true, + Description: "portgroup to attach the nic to. Do not set if you set distributed_switch_port.", + }, + "distributed_switch_port": { + Type: schema.TypeString, + Optional: true, + Description: "UUID of the DVSwitch the nic will be attached to. Do not set if you set portgroup.", + }, + "distributed_port_group": { + Type: schema.TypeString, + Optional: true, + Description: "Key of the distributed portgroup the nic will connect to", + }, + "ipv4": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "dhcp": { + Type: schema.TypeBool, + Optional: true, + Description: "Use DHCP to configure the interface's IPv4 stack.", + }, + "ip": { + Type: schema.TypeString, + Optional: true, + Description: "address of the interface, if DHCP is not set.", + }, + "netmask": { + Type: schema.TypeString, + Optional: true, + Description: "netmask of the interface, if DHCP is not set.", + }, + "gw": { + Type: schema.TypeString, + Optional: true, + Description: "IP address of the default gateway, if DHCP is not set.", + }, + }}, + }, + "ipv6": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "dhcp": { + Type: schema.TypeBool, + Optional: true, + Description: "Use DHCP to configure the interface's IPv4 stack.", + }, + "autoconfig": { + Type: schema.TypeBool, + Optional: true, + Description: "Use IPv6 Autoconfiguration (RFC2462).", + }, + "addresses": { + Type: schema.TypeList, + Optional: true, + Description: "List of IPv6 addresses", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if strings.ToLower(old) == strings.ToLower(new) { + return true + } + return false + }, + }, + "gw": { + Type: schema.TypeString, + Optional: true, + Description: "IP address of the default gateway, if DHCP or autoconfig is not set.", + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if strings.ToLower(old) == strings.ToLower(new) { + return true + } + return false + }, + }, + }}, + }, + "mac": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "MAC address of the interface.", + }, + "mtu": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "MTU of the interface.", + }, + "vmotion": { + Type: schema.TypeBool, + Optional: true, + Description: "Use this interface for vmotion.", + Default: false, + }, + "netstack": { + Type: schema.TypeString, + Optional: true, + Description: "TCP/IP stack setting for this interface. Possible values are 'default', 'vmotion', 'provisioning'", + Default: "default", + ForceNew: true, + }, + } + return sch +} + +func updateVNic(d *schema.ResourceData, meta interface{}) (string, error) { + client := meta.(*VSphereClient).vimClient + idParts := strings.Split(d.Id(), "_") + hostId := idParts[0] + nicId := idParts[1] + ctx := context.TODO() + + nic, err := getNicSpecFromSchema(d) + if err != nil { + return "", err + } + + hns, err := getHostNetworkSystem(client, hostId) + if err != nil { + return "", err + } + + err = hns.UpdateVirtualNic(ctx, nicId, *nic) + if err != nil { + return "", err + } + + vmotionEnabled := d.Get("vmotion").(bool) + if vmotionEnabled { + err := enableVmotion(client, hostId, nicId) + if err != nil { + return "", err + } + } + + return nicId, nil +} + +func createVNic(d *schema.ResourceData, meta interface{}) (string, error) { + client := meta.(*VSphereClient).vimClient + ctx := context.TODO() + + nic, err := getNicSpecFromSchema(d) + if err != nil { + return "", err + } + + hostId := d.Get("host").(string) + hns, err := getHostNetworkSystem(client, hostId) + if err != nil { + return "", err + } + + portgroup := d.Get("portgroup").(string) + //return hns.AddVirtualNic(ctx, portgroup, *nic) + nicId, err := hns.AddVirtualNic(ctx, portgroup, *nic) + if err != nil { + return "", err + } + d.SetId(fmt.Sprintf("%s_%s", hostId, nicId)) + return nicId, nil +} + +func removeVnic(client *govmomi.Client, hostId, nicId string) error { + hns, err := getHostNetworkSystem(client, hostId) + if err != nil { + return err + } + + return hns.RemoveVirtualNic(context.TODO(), nicId) +} + +func enableVmotion(client *govmomi.Client, hostId, nicId string) error { + ctx := context.TODO() + cm, err := getHostConfigManager(client, hostId) + if err != nil { + return err + } + hvnm, err := cm.VirtualNicManager(ctx) + if err != nil { + return err + } + err = hvnm.SelectVnic(ctx, "vmotion", nicId) + if err != nil { + return err + } + + return nil +} + +func getHostConfigManager(client *govmomi.Client, hostId string) (*object.HostConfigManager, error) { + host, err := hostsystem.FromID(client, hostId) + if err != nil { + return nil, err + } + cmRef := host.ConfigManager().Reference() + cm := object.NewHostConfigManager(client.Client, cmRef) + + return cm, nil +} + +func getHostNetworkSystem(client *govmomi.Client, hostId string) (*object.HostNetworkSystem, error) { + ctx := context.TODO() + + host, err := hostsystem.FromID(client, hostId) + if err != nil { + return nil, err + } + cmRef := host.ConfigManager().Reference() + cm := object.NewHostConfigManager(client.Client, cmRef) + hns, err := cm.NetworkSystem(ctx) + if err != nil { + log.Printf("[DEBUG] Failed to access the host's NetworkSystem service: %s", err) + return nil, err + } + return hns, nil +} + +func getNicSpecFromSchema(d *schema.ResourceData) (*types.HostVirtualNicSpec, error) { + portgroup := d.Get("portgroup").(string) + dvp := d.Get("distributed_switch_port").(string) + dpg := d.Get("distributed_port_group").(string) + mac := d.Get("mac").(string) + mtu := int32(d.Get("mtu").(int)) + + if portgroup != "" && dvp != "" { + return nil, fmt.Errorf("portgroup and distributed_switch_port settings are mutually exclusive") + } + + var dvpPortConnection *types.DistributedVirtualSwitchPortConnection + if portgroup != "" { + dvpPortConnection = nil + } else { + dvpPortConnection = &types.DistributedVirtualSwitchPortConnection{ + SwitchUuid: dvp, + PortgroupKey: dpg, + } + } + + ipConfig := &types.HostIpConfig{} + routeConfig := &types.HostIpRouteConfig{} //routeConfig := r.IpRouteConfig.GetHostIpRouteConfig() + if ipv4, ok := d.GetOk("ipv4.0"); ok { + ipv4Config := ipv4.(map[string]interface{}) + + dhcp := ipv4Config["dhcp"].(bool) + ipv4Address := ipv4Config["ip"].(string) + ipv4Netmask := ipv4Config["netmask"].(string) + ipv4Gateway := ipv4Config["gw"].(string) + + if dhcp { + ipConfig.Dhcp = dhcp + } else { + if ipv4Address != "" && ipv4Netmask != "" { + ipConfig.IpAddress = ipv4Address + ipConfig.SubnetMask = ipv4Netmask + routeConfig.DefaultGateway = ipv4Gateway + } + } + } + + if ipv6, ok := d.GetOk("ipv6.0"); ok { + ipv6Spec := &types.HostIpConfigIpV6AddressConfiguration{} + ipv6Config := ipv6.(map[string]interface{}) + + dhcpv6 := ipv6Config["dhcp"].(bool) + autoconfig := ipv6Config["autoconfig"].(bool) + //ipv6addrs := ipv6Config["addresses"].([]interface{}) + ipv6Gateway := ipv6Config["gw"].(string) + ipv6Spec.DhcpV6Enabled = &dhcpv6 + ipv6Spec.AutoConfigurationEnabled = &autoconfig + + oldAddrsIntf, newAddrsIntf := d.GetChange("ipv6.0.addresses") + oldAddrs := oldAddrsIntf.([]interface{}) + newAddrs := newAddrsIntf.([]interface{}) + removeAddrs := make([]string, len(oldAddrs)) + addAddrs := make([]string, len(newAddrs)) + + // calculate addresses to remove + for _, old := range oldAddrs { + addrFound := false + for _, newAddr := range newAddrs { + if old == newAddr { + addrFound = true + break + } + } + if !addrFound { + removeAddrs = append(removeAddrs, old.(string)) + } + } + + // calculate addresses to add + for _, newAddr := range newAddrs { + addrFound := false + for _, old := range oldAddrs { + if newAddr == old { + addrFound = true + break + } + } + if !addrFound { + addAddrs = append(addAddrs, newAddr.(string)) + } + + } + + if len(removeAddrs) > 0 || len(addAddrs) > 0 { + addrs := make([]types.HostIpConfigIpV6Address, 0) + for _, oldAddr := range oldAddrs { + addrParts := strings.Split(oldAddr.(string), "/") + addr := addrParts[0] + prefix, err := strconv.ParseInt(addrParts[1], 0, 32) + if err != nil { + return nil, fmt.Errorf("error while parsing IPv6 address") + } + tmpAddr := types.HostIpConfigIpV6Address{ + IpAddress: strings.ToLower(addr), + PrefixLength: int32(prefix), + Origin: "manual", + Operation: "remove", + } + addrs = append(addrs, tmpAddr) + } + + for _, newAddr := range newAddrs { + addrParts := strings.Split(newAddr.(string), "/") + addr := addrParts[0] + prefix, err := strconv.ParseInt(addrParts[1], 0, 32) + if err != nil { + return nil, fmt.Errorf("error while parsing IPv6 address") + } + tmpAddr := types.HostIpConfigIpV6Address{ + IpAddress: strings.ToLower(addr), + PrefixLength: int32(prefix), + Origin: "manual", + Operation: "add", + } + addrs = append(addrs, tmpAddr) + } + ipv6Spec.IpV6Address = addrs + } + //if len(ipv6addrs) > 0 { + // addrs := make([]types.HostIpConfigIpV6Address, len(ipv6addrs)) + // for i := range ipv6addrs { + // addrParts := strings.Split(ipv6addrs[i].(string), "/") + // addr := addrParts[0] + // prefix, err := strconv.ParseInt(addrParts[1], 0, 32) + // if err != nil { + // return nil, fmt.Errorf("error while parsing IPv6 address") + // } + // addrs[i] = types.HostIpConfigIpV6Address{ + // IpAddress: strings.ToLower(addr), + // PrefixLength: int32(prefix), + // Origin: "manual", + // Operation: "add", + // } + // log.Printf("[DEBUG] Added IP %s/%d in list", addr, prefix) + // } + routeConfig.IpV6DefaultGateway = ipv6Gateway + //} + ipConfig.IpV6Config = ipv6Spec + } + + r := &types.HostVirtualNicIpRouteSpec{ + IpRouteConfig: routeConfig, + } + + netStackInstance := d.Get("netstack").(string) + + vnic := &types.HostVirtualNicSpec{ + Ip: ipConfig, + Mac: mac, + Mtu: mtu, + Portgroup: portgroup, + DistributedVirtualPort: dvpPortConnection, + IpRouteSpec: r, + NetStackInstanceKey: netStackInstance, + } + return vnic, nil + +} + +func getVnicFromHost(ctx context.Context, client *govmomi.Client, hostId, nicId string) (*types.HostVirtualNic, error) { + host, err := hostsystem.FromID(client, hostId) + if err != nil { + return nil, err + } + + var hostProps mo.HostSystem + err = host.Properties(ctx, host.Reference(), nil, &hostProps) + if err != nil { + log.Printf("[DEBUG] Failed to get the host's properties: %s", err) + return nil, err + } + vNics := hostProps.Config.Network.Vnic + nicIdx := -1 + for idx, vnic := range vNics { + log.Printf("[DEBUG] Evaluating nic: %s", vnic.Device) + if vnic.Device == nicId { + nicIdx = idx + break + } + } + + if nicIdx == -1 { + return nil, fmt.Errorf("VMKernel interface with id %s not found", nicId) + } + return &vNics[nicIdx], nil +} diff --git a/vsphere/resource_vsphere_vnic_test.go b/vsphere/resource_vsphere_vnic_test.go new file mode 100644 index 000000000..23e09696e --- /dev/null +++ b/vsphere/resource_vsphere_vnic_test.go @@ -0,0 +1,421 @@ +package vsphere + +import ( + "context" + "errors" + "fmt" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/vmware/govmomi" + "os" + "strconv" + "strings" + "testing" +) + +type genTfConfig func(string) string + +func generateSteps(cfgFunc genTfConfig, netstack string) []resource.TestStep { + out := make([]resource.TestStep, 0) + for _, ipv4 := range []string{"dhcp", "192.0.2.10|255.255.255.0|192.0.2.1", ""} { + for _, ipv6 := range []string{"autoconfig", "dhcp", "2001:DB8::10/32|2001:DB8::1", ""} { + if ipv4 == "" && ipv6 == "" { + continue + } + cfg := combineSnippets(ipv4Snippet(ipv4), + ipv6Snippet(ipv6), + netstackSnippet(netstack)) + out = append(out, []resource.TestStep{ + resource.TestStep{ + Config: cfgFunc(cfg), + Check: resource.ComposeTestCheckFunc( + testAccVsphereVNicNetworkSettings("vsphere_vnic.v1", ipv4, ipv6, netstack), + ), + }, + }...) + } + } + return out +} + +func TestAccResourceVSphereVNic_dvs(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccVSphereVNicDestroy, + Steps: generateSteps(testAccVSphereVNicConfig_dvs, "defaultTcpipStack"), + }) +} + +func TestAccResourceVSphereVNic_dvs_vmotion(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccVSphereVNicDestroy, + Steps: generateSteps(testAccVSphereVNicConfig_dvs, "vmotion"), + }) +} + +func TestAccResourceVSphereVNic_hvs(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccVSphereVNicDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVSphereVNicConfig_hvs(combineSnippets( + ipv4Snippet("192.0.2.10|255.255.255.0|192.0.2.1"), + "", + netstackSnippet("defaultTcpipStack"))), + Check: resource.ComposeTestCheckFunc( + testAccVsphereVNicNetworkSettings("vsphere_vnic.v1", + "192.0.2.10|255.255.255.0|192.0.2.1", + "", + "defaultTcpipStack"), + ), + }, + }, + }) +} + +func TestAccResourceVSphereVNic_hvs_vmotion(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccVSphereVNicDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVSphereVNicConfig_hvs(combineSnippets( + ipv4Snippet("192.0.2.10|255.255.255.0|192.0.2.1"), + "", + netstackSnippet("vmotion"))), + Check: resource.ComposeTestCheckFunc( + testAccVsphereVNicNetworkSettings("vsphere_vnic.v1", + "192.0.2.10|255.255.255.0|192.0.2.1", + "", + "vmotion"), + ), + }, + }, + }) +} + +func testAccVsphereVNicNetworkSettings(name, ipv4State, ipv6State, netstack string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + + if !ok { + return fmt.Errorf("%s key not found on the server", name) + } + idParts := strings.Split(rs.Primary.ID, "_") + hostId := idParts[0] + vmnicId := idParts[1] + client := testAccProvider.Meta().(*VSphereClient).vimClient + vnic, err := getVnicFromHost(context.TODO(), client, hostId, vmnicId) + if err != nil { + return err + } + + switch ipv4State { + case "dhcp": + if !vnic.Spec.Ip.Dhcp { + return fmt.Errorf("ipv4 network error, expected dhcp") + } + case "": + break + default: + if vnic.Spec.Ip.Dhcp { + return fmt.Errorf("ipv4 network error, expected static") + } + if ipv4State == "static" { + break + } + addrBits := strings.Split(ipv4State, "|") + if len(addrBits) != 3 { + return fmt.Errorf("ipv4 test error, invalid parameter: %s", ipv4State) + } + ip := addrBits[0] + netmask := addrBits[1] + gw := addrBits[2] + + routeConfig := vnic.Spec.IpRouteSpec.IpRouteConfig.GetHostIpRouteConfig() + if ip != vnic.Spec.Ip.IpAddress || netmask != vnic.Spec.Ip.SubnetMask || gw != routeConfig.DefaultGateway { + return fmt.Errorf( + "ipv4 network error, static config mismatch. ip %s vs %s, netmask %s vs %s, gw %s vs %s", + ip, vnic.Spec.Ip.IpAddress, + netmask, vnic.Spec.Ip.SubnetMask, + gw, routeConfig.DefaultGateway) + } + } + + switch ipv6State { + case "dhcp": + if !*vnic.Spec.Ip.IpV6Config.DhcpV6Enabled { + return fmt.Errorf("ipv6 network error, expected dhcp") + } + case "autoconfig": + if !*vnic.Spec.Ip.IpV6Config.AutoConfigurationEnabled { + return fmt.Errorf("ipv6 network error, expected autoconfig") + } + case "": + break + default: + if *vnic.Spec.Ip.IpV6Config.AutoConfigurationEnabled || *vnic.Spec.Ip.IpV6Config.DhcpV6Enabled { + return fmt.Errorf("ipv6 network error, expected static configuration") + } + if ipv6State == "static" { + break + } + addrBits := strings.Split(ipv6State, "|") + if len(addrBits) != 2 { + return fmt.Errorf("ipv4 test error, invalid parameter: %s", ipv6State) + } + ipParts := strings.Split(addrBits[0], "/") + ip := ipParts[0] + prefix, err := strconv.ParseInt(ipParts[1], 10, 32) + if err != nil { + return fmt.Errorf("error while parsing prefix: %s", err) + } + gw := addrBits[1] + routeConfig := vnic.Spec.IpRouteSpec.IpRouteConfig.GetHostIpRouteConfig() + + // loop through ipv6 IPs until we find our own + addrFound := false + for _, ipv6addr := range vnic.Spec.Ip.IpV6Config.IpV6Address { + if strings.ToLower(ipv6addr.IpAddress) == strings.ToLower(ip) { + if ipv6addr.PrefixLength != int32(prefix) || + strings.ToLower(routeConfig.IpV6DefaultGateway) != strings.ToLower(gw) { + return fmt.Errorf( + "ipv6 network error, static config mismatch. prefix length %d vs %d, gw %s vs %s", + prefix, ipv6addr.PrefixLength, + gw, routeConfig.IpV6DefaultGateway) + } + addrFound = true + break + } + } + if !addrFound { + return fmt.Errorf("ipv6 network error, could not find %s assigned to the interface", ip) + } + } + + configuredNetstack := vnic.Spec.NetStackInstanceKey + if netstack != configuredNetstack { + return fmt.Errorf("netstack mismatch. expected %s found %s", netstack, configuredNetstack) + } + + return nil + } +} + +func testAccVSphereVNicDestroy(s *terraform.State) error { + message := "" + for _, rs := range s.RootModule().Resources { + if rs.Type != "vsphere_vnic" { + continue + } + nicId := rs.Primary.ID + client := testAccProvider.Meta().(*VSphereClient).vimClient + res, err := nicExists(client, nicId) + if err != nil { + return err + } + + if res { + message += fmt.Sprintf("Host with ID %s was found", nicId) + } + } + if message != "" { + return errors.New(message) + } + return nil +} + +func nicExists(client *govmomi.Client, nicId string) (bool, error) { + toks := strings.Split(nicId, "_") + vmnicId := toks[1] + hostId := toks[0] + _, err := getVnicFromHost(context.TODO(), client, hostId, vmnicId) + if err != nil { + if err.Error() == fmt.Sprintf("vNic interface with id %s not found", vmnicId) { + return false, nil + } + return false, err + } + + return true, nil +} + +func testAccVSphereVNicConfig_hvs(netConfig string) string { + return fmt.Sprintf(` + data "vsphere_datacenter" "dc" { + name = "%s" + } + + data "vsphere_host" "h1" { + name = "%s" + datacenter_id = data.vsphere_datacenter.dc.id + } + + + resource "vsphere_host_virtual_switch" "hvs1" { + name = "hashi-dc_HPG0" + host_system_id = data.vsphere_host.h1.id + network_adapters = ["%s", "%s"] + active_nics = ["%s"] + standby_nics = ["%s"] + } + + resource "vsphere_host_port_group" "p1" { + name = "ko-pg" + virtual_switch_name = vsphere_host_virtual_switch.hvs1.name + host_system_id = data.vsphere_host.h1.id + } + + resource "vsphere_vnic" "v1" { + host = data.vsphere_host.h1.id + portgroup = vsphere_host_port_group.p1.name + %s + } + `, os.Getenv("VSPHERE_DATACENTER"), + os.Getenv("VSPHERE_ESXI_HOST3"), + os.Getenv("VSPHERE_HOST_NIC0"), + os.Getenv("VSPHERE_HOST_NIC1"), + os.Getenv("VSPHERE_HOST_NIC0"), + os.Getenv("VSPHERE_HOST_NIC1"), + netConfig) +} + +func testAccVSphereVNicConfig_dvs(netConfig string) string { + return fmt.Sprintf(` + data "vsphere_datacenter" "dc" { + name = "%s" + } + + data "vsphere_host" "h1" { + name = "%s" + datacenter_id = data.vsphere_datacenter.dc.id + } + + + resource "vsphere_distributed_virtual_switch" "d1" { + name = "hashi-dc_DVPG0" + datacenter_id = data.vsphere_datacenter.dc.id + host { + host_system_id = data.vsphere_host.h1.id + devices = ["%s"] + } + } + + resource "vsphere_distributed_port_group" "p1" { + name = "ko-pg" + vlan_id = 1234 + distributed_virtual_switch_uuid = vsphere_distributed_virtual_switch.d1.id + } + + resource "vsphere_vnic" "v1" { + host = data.vsphere_host.h1.id + distributed_switch_port = vsphere_distributed_virtual_switch.d1.id + distributed_port_group = vsphere_distributed_port_group.p1.id + %s + } + `, os.Getenv("VSPHERE_DATACENTER"), + os.Getenv("VSPHERE_ESXI_HOST"), + os.Getenv("VSPHERE_HOST_NIC1"), + netConfig) +} + +func combineSnippets(snippets ...string) string { + out := "" + for _, snippet := range snippets { + out = fmt.Sprintf("%s\n%s", out, snippet) + } + return out +} + +func ipv4Snippet(payload string) string { + switch payload { + case "dhcp": + return ipv4DHCPSnippet() + case "": + return "" + default: + addrBits := strings.Split(payload, "|") + ip := addrBits[0] + netmask := addrBits[1] + gw := addrBits[2] + return ipv4StaticSnippet(ip, netmask, gw) + } +} + +func ipv4StaticSnippet(ip, netmask, gw string) string { + return fmt.Sprintf(` + ipv4 { + ip = "%s" + netmask = "%s" + gw = "%s" + } + `, ip, netmask, gw) +} + +func ipv4DHCPSnippet() string { + return ` + ipv4 { + dhcp = true + }` +} + +func ipv6Snippet(payload string) string { + switch payload { + case "dhcp": + return ipv6DHCPSnippet() + case "autoconfig": + return ipv6AutoconfigSnippet() + case "": + return "" + default: + addrBits := strings.Split(payload, "|") + ip := addrBits[0] + gw := addrBits[1] + return ipv6StaticSnippet(ip, gw) + } +} + +func ipv6DHCPSnippet() string { + return ` + ipv6 { + dhcp = true + }` +} + +func ipv6AutoconfigSnippet() string { + return ` + ipv6 { + autoconfig = true + }` +} + +func ipv6StaticSnippet(ip, gw string) string { + return fmt.Sprintf(` + ipv6 { + addresses = ["%s"] + gw = "%s" + }`, ip, gw) +} + +func netstackSnippet(stack string) string { + if stack == "" { + stack = "defaultTcpipStack" + } + return fmt.Sprintf(` + netstack = "%s" + `, stack) +}