diff --git a/docs/book/src/clustercloudstack/configuration.md b/docs/book/src/clustercloudstack/configuration.md index 0734d0bf..1c8b0c6a 100644 --- a/docs/book/src/clustercloudstack/configuration.md +++ b/docs/book/src/clustercloudstack/configuration.md @@ -143,6 +143,45 @@ After setting the environment variables, execute the following command to genera clusterctl generate cluster capc-cluster --flavor with-kube-vip > capc-cluster-spec.yaml ``` +##### Option for Multiple Networks + +Multiple networks can be specified at each node configuration in CloudStackMachineTemplate. +This is configured under `spec.template.spec.networks`, where you can list one or more networks by name or id, +and optionally assign static IP addresses. + +When defining multiple networks for a VM in CAPC, the first network listed under `spec.template.spec.networks` is treated as +the primary network. This primary network must match the network defined in the failure domain’s zone (`failureDomains[].zone.network`), +either by name or by ID. It is used as the default NIC and is critical for VM boot and cluster communication. + +Any networks listed after the primary are considered extra networks. These extra networks are attached as secondary NICs +on the VM and can be used for purposes such as service segmentation or additional routing. Each network entry, primary or +extra can optionally include a static IP address. If an IP is not specified, CloudStack will dynamically allocate one. + +For example: + +```yaml +apiVersion: infrastructure.cluster.x-k8s.io/v1beta3 +kind: CloudStackMachineTemplate +metadata: + name: capc-cluster-control-plane + namespace: default +spec: + template: + spec: + offering: + name: Large Instance + networks: + - name: cloudstack-network # (optional) default primary network; must match with network at failureDomains.zone.network.name + ip: 10.1.1.21 # (optional) static IP in the primary network + + # Additional (extra) networks can be specified below. Use either 'name' or 'id', and optionally an 'ip'. + - name: cloudstack-network-2 # (optional) extra network by name + ip: 10.1.1.31 # (optional) static IP in this network + + - id: a1b2c3d4-5678-90ef-gh12-3456789ijklm # (optional) extra network by ID + ip: 10.1.1.41 # (optional) static IP in this network +``` + #### CloudStack Endpoint Credentials Secret (*optional for provided templates when used with provided getting-started process*) A reference to a Kubernetes Secret containing a YAML object containing credentials for accessing a particular CloudStack diff --git a/pkg/cloud/instance.go b/pkg/cloud/instance.go index 3da2705c..ad6a7cdc 100644 --- a/pkg/cloud/instance.go +++ b/pkg/cloud/instance.go @@ -19,6 +19,7 @@ package cloud import ( "encoding/base64" "fmt" + "net" "strconv" "strings" @@ -44,7 +45,15 @@ func setMachineDataFromVMMetrics(vmResponse *cloudstack.VirtualMachinesMetric, c csMachine.Spec.ProviderID = ptr.To(fmt.Sprintf("cloudstack:///%s", vmResponse.Id)) // InstanceID is later used as required parameter to destroy VM. csMachine.Spec.InstanceID = ptr.To(vmResponse.Id) - csMachine.Status.Addresses = []corev1.NodeAddress{{Type: corev1.NodeInternalIP, Address: vmResponse.Ipaddress}} + csMachine.Status.Addresses = []corev1.NodeAddress{} + for _, nic := range vmResponse.Nic { + if nic.Ipaddress != "" { + csMachine.Status.Addresses = append(csMachine.Status.Addresses, corev1.NodeAddress{ + Type: corev1.NodeInternalIP, + Address: nic.Ipaddress, + }) + } + } newInstanceState := vmResponse.State if newInstanceState != csMachine.Status.InstanceState || (newInstanceState != "" && csMachine.Status.InstanceStateLastUpdated.IsZero()) { csMachine.Status.InstanceState = newInstanceState @@ -302,38 +311,151 @@ func (c *client) CheckLimits( return nil } -func (c *client) resolveNetworkIDByName(name string) (string, error) { +func (c *client) isFreeIPAvailable(networkID, ip string) (bool, error) { + params := c.cs.Address.NewListPublicIpAddressesParams() + params.SetNetworkid(networkID) + params.SetAllocatedonly(false) + params.SetForvirtualnetwork(false) + params.SetListall(true) + + if ip != "" { + params.SetIpaddress(ip) + } + + resp, err := c.cs.Address.ListPublicIpAddresses(params) + if err != nil { + return false, errors.Wrapf(err, "failed to list public IP addresses for network %q", networkID) + } + + for _, addr := range resp.PublicIpAddresses { + if addr.State == "Free" { + return true, nil + } + } + + return false, nil +} + +func (c *client) buildIPEntry(resolvedNet *cloudstack.Network, ip string) (map[string]string, error) { + if ip != "" { + if err := validateIPInCIDR(ip, resolvedNet.Cidr); err != nil { + return nil, err + } + } + + if resolvedNet.Type == NetworkTypeShared { + isAvailable, err := c.isFreeIPAvailable(resolvedNet.Id, ip) + if err != nil { + return nil, err + } + if !isAvailable { + if ip != "" { + return nil, errors.Errorf("IP %q is already allocated in network %q or out of range", ip, resolvedNet.Id) + } + return nil, errors.Errorf("no free IPs available in network %q", resolvedNet.Id) + } + } + + entry := map[string]string{ + "networkid": resolvedNet.Id, + } + if ip != "" { + entry["ip"] = ip + } + return entry, nil +} + +func (c *client) resolveNetworkByName(name string) (*cloudstack.Network, error) { net, count, err := c.cs.Network.GetNetworkByName(name, cloudstack.WithProject(c.user.Project.ID)) if err != nil { - return "", errors.Wrapf(err, "failed to look up network %q", name) + return nil, errors.Wrapf(err, "failed to look up network %q", name) } if count != 1 { - return "", errors.Errorf("expected 1 network named %q, but got %d", name, count) + return nil, errors.Errorf("expected 1 network named %q, but got %d", name, count) } - return net.Id, nil + return net, nil } func (c *client) buildIPToNetworkList(csMachine *infrav1.CloudStackMachine) ([]map[string]string, error) { - ipToNetworkList := []map[string]string{} + var ipToNetworkList []map[string]string for _, net := range csMachine.Spec.Networks { - networkID := net.ID - if networkID == "" { - var err error - networkID, err = c.resolveNetworkIDByName(net.Name) - if err != nil { - return nil, err - } + resolvedNet, err := c.resolveNetwork(net) + if err != nil { + return nil, err } - entry := map[string]string{"networkid": networkID} - if net.IP != "" { - entry["ip"] = net.IP + + entry, err := c.buildIPEntry(resolvedNet, net.IP) + if err != nil { + return nil, err } + ipToNetworkList = append(ipToNetworkList, entry) } + return ipToNetworkList, nil } +func (c *client) resolveNetwork(net infrav1.NetworkSpec) (*cloudstack.Network, error) { + if net.ID == "" { + return c.resolveNetworkByName(net.Name) + } + + resolvedNet, _, err := c.cs.Network.GetNetworkByID(net.ID, cloudstack.WithProject(c.user.Project.ID)) + if err != nil { + return nil, errors.Wrapf(err, "failed to get network %q by ID", net.ID) + } + return resolvedNet, nil +} + +func validateIPInCIDR(ipStr, cidrStr string) error { + ip := net.ParseIP(ipStr) + if ip == nil { + return errors.Errorf("invalid IP address %q", ipStr) + } + + _, cidr, err := net.ParseCIDR(cidrStr) + if err != nil { + return errors.Wrapf(err, "invalid CIDR %q", cidrStr) + } + + if !cidr.Contains(ip) { + return errors.Errorf("IP %q is not within network CIDR %q", ipStr, cidrStr) + } + + return nil +} + +func (c *client) configureNetworkParams( + p *cloudstack.DeployVirtualMachineParams, + csMachine *infrav1.CloudStackMachine, + fd *infrav1.CloudStackFailureDomain, +) error { + if len(csMachine.Spec.Networks) == 0 { + if fd.Spec.Zone.Network.ID != "" { + p.SetNetworkids([]string{fd.Spec.Zone.Network.ID}) + } + } else { + firstNetwork := csMachine.Spec.Networks[0] + zoneNet := fd.Spec.Zone.Network + + if zoneNet.ID != "" && firstNetwork.ID != "" && firstNetwork.ID != zoneNet.ID { + return errors.Errorf("first network ID %q does not match zone network ID %q", firstNetwork.ID, zoneNet.ID) + } + if zoneNet.Name != "" && firstNetwork.Name != "" && firstNetwork.Name != zoneNet.Name { + return errors.Errorf("first network name %q does not match zone network name %q", firstNetwork.Name, zoneNet.Name) + } + + ipToNetworkList, err := c.buildIPToNetworkList(csMachine) + if err != nil { + return err + } + p.SetIptonetworklist(ipToNetworkList) + } + + return nil +} + // DeployVM will create a VM instance, // and sets the infrastructure machine spec and status accordingly. func (c *client) DeployVM( @@ -355,14 +477,8 @@ func (c *client) DeployVM( p := c.cs.VirtualMachine.NewDeployVirtualMachineParams(offering.Id, templateID, fd.Spec.Zone.ID) - if len(csMachine.Spec.Networks) == 0 && fd.Spec.Zone.Network.ID != "" { - p.SetNetworkids([]string{fd.Spec.Zone.Network.ID}) - } else { - ipToNetworkList, err := c.buildIPToNetworkList(csMachine) - if err != nil { - return err - } - p.SetIptonetworklist(ipToNetworkList) + if err := c.configureNetworkParams(p, csMachine, fd); err != nil { + return err } setIfNotEmpty(csMachine.Name, p.SetName)