Skip to content

Commit ee61587

Browse files
authored
Merge pull request #453 from shapeblue/multinic-validations-template
Added validations and templates for multiple networks configuration
2 parents 2bcce63 + 054530e commit ee61587

File tree

2 files changed

+179
-24
lines changed

2 files changed

+179
-24
lines changed

docs/book/src/clustercloudstack/configuration.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,45 @@ After setting the environment variables, execute the following command to genera
143143
clusterctl generate cluster capc-cluster --flavor with-kube-vip > capc-cluster-spec.yaml
144144
```
145145
146+
##### Option for Multiple Networks
147+
148+
Multiple networks can be specified at each node configuration in CloudStackMachineTemplate.
149+
This is configured under `spec.template.spec.networks`, where you can list one or more networks by name or id,
150+
and optionally assign static IP addresses.
151+
152+
When defining multiple networks for a VM in CAPC, the first network listed under `spec.template.spec.networks` is treated as
153+
the primary network. This primary network must match the network defined in the failure domain’s zone (`failureDomains[].zone.network`),
154+
either by name or by ID. It is used as the default NIC and is critical for VM boot and cluster communication.
155+
156+
Any networks listed after the primary are considered extra networks. These extra networks are attached as secondary NICs
157+
on the VM and can be used for purposes such as service segmentation or additional routing. Each network entry, primary or
158+
extra can optionally include a static IP address. If an IP is not specified, CloudStack will dynamically allocate one.
159+
160+
For example:
161+
162+
```yaml
163+
apiVersion: infrastructure.cluster.x-k8s.io/v1beta3
164+
kind: CloudStackMachineTemplate
165+
metadata:
166+
name: capc-cluster-control-plane
167+
namespace: default
168+
spec:
169+
template:
170+
spec:
171+
offering:
172+
name: Large Instance
173+
networks:
174+
- name: cloudstack-network # (optional) default primary network; must match with network at failureDomains.zone.network.name
175+
ip: 10.1.1.21 # (optional) static IP in the primary network
176+
177+
# Additional (extra) networks can be specified below. Use either 'name' or 'id', and optionally an 'ip'.
178+
- name: cloudstack-network-2 # (optional) extra network by name
179+
ip: 10.1.1.31 # (optional) static IP in this network
180+
181+
- id: a1b2c3d4-5678-90ef-gh12-3456789ijklm # (optional) extra network by ID
182+
ip: 10.1.1.41 # (optional) static IP in this network
183+
```
184+
146185
#### CloudStack Endpoint Credentials Secret (*optional for provided templates when used with provided getting-started process*)
147186
148187
A reference to a Kubernetes Secret containing a YAML object containing credentials for accessing a particular CloudStack

pkg/cloud/instance.go

Lines changed: 140 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package cloud
1919
import (
2020
"encoding/base64"
2121
"fmt"
22+
"net"
2223
"strconv"
2324
"strings"
2425

@@ -44,7 +45,15 @@ func setMachineDataFromVMMetrics(vmResponse *cloudstack.VirtualMachinesMetric, c
4445
csMachine.Spec.ProviderID = ptr.To(fmt.Sprintf("cloudstack:///%s", vmResponse.Id))
4546
// InstanceID is later used as required parameter to destroy VM.
4647
csMachine.Spec.InstanceID = ptr.To(vmResponse.Id)
47-
csMachine.Status.Addresses = []corev1.NodeAddress{{Type: corev1.NodeInternalIP, Address: vmResponse.Ipaddress}}
48+
csMachine.Status.Addresses = []corev1.NodeAddress{}
49+
for _, nic := range vmResponse.Nic {
50+
if nic.Ipaddress != "" {
51+
csMachine.Status.Addresses = append(csMachine.Status.Addresses, corev1.NodeAddress{
52+
Type: corev1.NodeInternalIP,
53+
Address: nic.Ipaddress,
54+
})
55+
}
56+
}
4857
newInstanceState := vmResponse.State
4958
if newInstanceState != csMachine.Status.InstanceState || (newInstanceState != "" && csMachine.Status.InstanceStateLastUpdated.IsZero()) {
5059
csMachine.Status.InstanceState = newInstanceState
@@ -302,38 +311,151 @@ func (c *client) CheckLimits(
302311
return nil
303312
}
304313

305-
func (c *client) resolveNetworkIDByName(name string) (string, error) {
314+
func (c *client) isFreeIPAvailable(networkID, ip string) (bool, error) {
315+
params := c.cs.Address.NewListPublicIpAddressesParams()
316+
params.SetNetworkid(networkID)
317+
params.SetAllocatedonly(false)
318+
params.SetForvirtualnetwork(false)
319+
params.SetListall(true)
320+
321+
if ip != "" {
322+
params.SetIpaddress(ip)
323+
}
324+
325+
resp, err := c.cs.Address.ListPublicIpAddresses(params)
326+
if err != nil {
327+
return false, errors.Wrapf(err, "failed to list public IP addresses for network %q", networkID)
328+
}
329+
330+
for _, addr := range resp.PublicIpAddresses {
331+
if addr.State == "Free" {
332+
return true, nil
333+
}
334+
}
335+
336+
return false, nil
337+
}
338+
339+
func (c *client) buildIPEntry(resolvedNet *cloudstack.Network, ip string) (map[string]string, error) {
340+
if ip != "" {
341+
if err := validateIPInCIDR(ip, resolvedNet.Cidr); err != nil {
342+
return nil, err
343+
}
344+
}
345+
346+
if resolvedNet.Type == NetworkTypeShared {
347+
isAvailable, err := c.isFreeIPAvailable(resolvedNet.Id, ip)
348+
if err != nil {
349+
return nil, err
350+
}
351+
if !isAvailable {
352+
if ip != "" {
353+
return nil, errors.Errorf("IP %q is already allocated in network %q or out of range", ip, resolvedNet.Id)
354+
}
355+
return nil, errors.Errorf("no free IPs available in network %q", resolvedNet.Id)
356+
}
357+
}
358+
359+
entry := map[string]string{
360+
"networkid": resolvedNet.Id,
361+
}
362+
if ip != "" {
363+
entry["ip"] = ip
364+
}
365+
return entry, nil
366+
}
367+
368+
func (c *client) resolveNetworkByName(name string) (*cloudstack.Network, error) {
306369
net, count, err := c.cs.Network.GetNetworkByName(name, cloudstack.WithProject(c.user.Project.ID))
307370
if err != nil {
308-
return "", errors.Wrapf(err, "failed to look up network %q", name)
371+
return nil, errors.Wrapf(err, "failed to look up network %q", name)
309372
}
310373
if count != 1 {
311-
return "", errors.Errorf("expected 1 network named %q, but got %d", name, count)
374+
return nil, errors.Errorf("expected 1 network named %q, but got %d", name, count)
312375
}
313-
return net.Id, nil
376+
return net, nil
314377
}
315378

316379
func (c *client) buildIPToNetworkList(csMachine *infrav1.CloudStackMachine) ([]map[string]string, error) {
317-
ipToNetworkList := []map[string]string{}
380+
var ipToNetworkList []map[string]string
318381

319382
for _, net := range csMachine.Spec.Networks {
320-
networkID := net.ID
321-
if networkID == "" {
322-
var err error
323-
networkID, err = c.resolveNetworkIDByName(net.Name)
324-
if err != nil {
325-
return nil, err
326-
}
383+
resolvedNet, err := c.resolveNetwork(net)
384+
if err != nil {
385+
return nil, err
327386
}
328-
entry := map[string]string{"networkid": networkID}
329-
if net.IP != "" {
330-
entry["ip"] = net.IP
387+
388+
entry, err := c.buildIPEntry(resolvedNet, net.IP)
389+
if err != nil {
390+
return nil, err
331391
}
392+
332393
ipToNetworkList = append(ipToNetworkList, entry)
333394
}
395+
334396
return ipToNetworkList, nil
335397
}
336398

399+
func (c *client) resolveNetwork(net infrav1.NetworkSpec) (*cloudstack.Network, error) {
400+
if net.ID == "" {
401+
return c.resolveNetworkByName(net.Name)
402+
}
403+
404+
resolvedNet, _, err := c.cs.Network.GetNetworkByID(net.ID, cloudstack.WithProject(c.user.Project.ID))
405+
if err != nil {
406+
return nil, errors.Wrapf(err, "failed to get network %q by ID", net.ID)
407+
}
408+
return resolvedNet, nil
409+
}
410+
411+
func validateIPInCIDR(ipStr, cidrStr string) error {
412+
ip := net.ParseIP(ipStr)
413+
if ip == nil {
414+
return errors.Errorf("invalid IP address %q", ipStr)
415+
}
416+
417+
_, cidr, err := net.ParseCIDR(cidrStr)
418+
if err != nil {
419+
return errors.Wrapf(err, "invalid CIDR %q", cidrStr)
420+
}
421+
422+
if !cidr.Contains(ip) {
423+
return errors.Errorf("IP %q is not within network CIDR %q", ipStr, cidrStr)
424+
}
425+
426+
return nil
427+
}
428+
429+
func (c *client) configureNetworkParams(
430+
p *cloudstack.DeployVirtualMachineParams,
431+
csMachine *infrav1.CloudStackMachine,
432+
fd *infrav1.CloudStackFailureDomain,
433+
) error {
434+
if len(csMachine.Spec.Networks) == 0 {
435+
if fd.Spec.Zone.Network.ID != "" {
436+
p.SetNetworkids([]string{fd.Spec.Zone.Network.ID})
437+
}
438+
} else {
439+
firstNetwork := csMachine.Spec.Networks[0]
440+
zoneNet := fd.Spec.Zone.Network
441+
442+
if zoneNet.ID != "" && firstNetwork.ID != "" && firstNetwork.ID != zoneNet.ID {
443+
return errors.Errorf("first network ID %q does not match zone network ID %q", firstNetwork.ID, zoneNet.ID)
444+
}
445+
if zoneNet.Name != "" && firstNetwork.Name != "" && firstNetwork.Name != zoneNet.Name {
446+
return errors.Errorf("first network name %q does not match zone network name %q", firstNetwork.Name, zoneNet.Name)
447+
}
448+
449+
ipToNetworkList, err := c.buildIPToNetworkList(csMachine)
450+
if err != nil {
451+
return err
452+
}
453+
p.SetIptonetworklist(ipToNetworkList)
454+
}
455+
456+
return nil
457+
}
458+
337459
// DeployVM will create a VM instance,
338460
// and sets the infrastructure machine spec and status accordingly.
339461
func (c *client) DeployVM(
@@ -355,14 +477,8 @@ func (c *client) DeployVM(
355477

356478
p := c.cs.VirtualMachine.NewDeployVirtualMachineParams(offering.Id, templateID, fd.Spec.Zone.ID)
357479

358-
if len(csMachine.Spec.Networks) == 0 && fd.Spec.Zone.Network.ID != "" {
359-
p.SetNetworkids([]string{fd.Spec.Zone.Network.ID})
360-
} else {
361-
ipToNetworkList, err := c.buildIPToNetworkList(csMachine)
362-
if err != nil {
363-
return err
364-
}
365-
p.SetIptonetworklist(ipToNetworkList)
480+
if err := c.configureNetworkParams(p, csMachine, fd); err != nil {
481+
return err
366482
}
367483

368484
setIfNotEmpty(csMachine.Name, p.SetName)

0 commit comments

Comments
 (0)