Skip to content

Commit

Permalink
feat: allow template selection based on tags
Browse files Browse the repository at this point in the history
* `sourceNode + templateID` and `templateSelector` are mutually exclusive
* automatically detects both `sourceNode` + `templateID`
* errors out if anything but one (1) VM template with desired flags was found
  • Loading branch information
pborn-ionos committed Dec 3, 2024
1 parent 51a9d0a commit be8cc1d
Show file tree
Hide file tree
Showing 15 changed files with 507 additions and 9 deletions.
25 changes: 24 additions & 1 deletion api/v1alpha1/proxmoxmachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,16 @@ type VirtualMachineCloneSpec struct {
// will be cloned onto the same node as SourceNode.
//
// +kubebuilder:validation:MinLength=1
// +optional
SourceNode string `json:"sourceNode"`

// TemplateID the vm_template vmid used for cloning a new VM.
// +optional
TemplateID *int32 `json:"templateID,omitempty"`

// +optional
TemplateSelector *TemplateSelector `json:"templateSelector,omitempty"`

// Description for the new VM.
// +optional
Description *string `json:"description,omitempty"`
Expand Down Expand Up @@ -202,6 +206,14 @@ type VirtualMachineCloneSpec struct {
Target *string `json:"target,omitempty"`
}

// TemplateSelector defines tags for looking up images.
type TemplateSelector struct {
// Specifies all tags to look for, when looking up the VM template.
//
// +kubebuilder:validation:MinItems=1
MatchTags []string `json:"matchTags"`
}

// NetworkSpec defines the virtual machine's network configuration.
type NetworkSpec struct {
// Default is the default network device,
Expand Down Expand Up @@ -526,9 +538,20 @@ func (r *ProxmoxMachine) GetTemplateID() int32 {
return -1
}

// GetTemplateSelectorTags get the tags, the desired vm template should have.
func (r *ProxmoxMachine) GetTemplateSelectorTags() []string {
if r.Spec.TemplateSelector != nil && r.Spec.TemplateSelector.MatchTags != nil {
return r.Spec.TemplateSelector.MatchTags
}
return nil
}

// GetNode get the Proxmox node used to provision this machine.
func (r *ProxmoxMachine) GetNode() string {
return r.Spec.SourceNode
if r.Spec.SourceNode != "" {
return r.Spec.SourceNode
}
return ""
}

// FormatSize returns the format required for the Proxmox API.
Expand Down
25 changes: 25 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -548,13 +548,25 @@ spec:
a new VM.
format: int32
type: integer
templateSelector:
description: TemplateSelector defines tags for looking up
images.
properties:
matchTags:
description: Specifies all tags to look for, when looking
up the VM template.
items:
type: string
minItems: 1
type: array
required:
- matchTags
type: object
virtualMachineID:
description: VirtualMachineID is the Proxmox identifier
for the ProxmoxMachine VM.
format: int64
type: integer
required:
- sourceNode
type: object
type: object
x-kubernetes-validations:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -588,13 +588,25 @@ spec:
for cloning a new VM.
format: int32
type: integer
templateSelector:
description: TemplateSelector defines tags for looking
up images.
properties:
matchTags:
description: Specifies all tags to look for,
when looking up the VM template.
items:
type: string
minItems: 1
type: array
required:
- matchTags
type: object
virtualMachineID:
description: VirtualMachineID is the Proxmox identifier
for the ProxmoxMachine VM.
format: int64
type: integer
required:
- sourceNode
type: object
type: object
x-kubernetes-validations:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,13 +513,24 @@ spec:
VM.
format: int32
type: integer
templateSelector:
description: TemplateSelector defines tags for looking up images.
properties:
matchTags:
description: Specifies all tags to look for, when looking up the
VM template.
items:
type: string
minItems: 1
type: array
required:
- matchTags
type: object
virtualMachineID:
description: VirtualMachineID is the Proxmox identifier for the ProxmoxMachine
VM.
format: int64
type: integer
required:
- sourceNode
type: object
x-kubernetes-validations:
- message: Must set full=true when specifying format
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -546,13 +546,25 @@ spec:
a new VM.
format: int32
type: integer
templateSelector:
description: TemplateSelector defines tags for looking up
images.
properties:
matchTags:
description: Specifies all tags to look for, when looking
up the VM template.
items:
type: string
minItems: 1
type: array
required:
- matchTags
type: object
virtualMachineID:
description: VirtualMachineID is the Proxmox identifier for
the ProxmoxMachine VM.
format: int64
type: integer
required:
- sourceNode
type: object
required:
- spec
Expand Down
7 changes: 7 additions & 0 deletions docs/advanced-setups.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ This behaviour can be configured in the `ProxmoxCluster` CR through the field `.

For example, setting it to `0` (zero), entirely disables scheduling based on memory. Alternatively, if you set it to any value greater than `0`, the scheduler will treat your host as it would have `${value}%` of memory. In real numbers that would mean, if you have a host with 64GB of memory and set the number to `300`, the scheduler would allow you to provision guests with a total of 192GB memory and therefore overprovision the host. (Use with caution! It's strongly suggested to have memory ballooning configured everywhere.). Or, if you were to set it to `95` for example, it would treat your host as it would only have 60,8GB of memory, and leave the remaining 3,2GB for the host.

## Template lookup based on Proxmox tags

Our provider is able to look up templates based on their attached tags, for `ProxmoxMachine` resources, that make use of an tag selector.

For example, you can set the `TEMPLATE_TAGS="tag1,tag2"` environment variable. Your custom image will then be used when using the [auto-image](https://github.com/ionos-cloud/cluster-api-provider-ionoscloud/blob/main/templates/cluster-template-auto-image.yaml) template.


## Proxmox RBAC with least privileges

For the Proxmox API user/token you create for CAPMOX, these are the minimum required permissions.
Expand Down
1 change: 1 addition & 0 deletions envfile.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export PROXMOX_TOKEN=""
export PROXMOX_SECRET=""
export PROXMOX_SOURCENODE="pve"
export TEMPLATE_VMID=100
export TEMPLATE_TAGS="tag1,tag2"
export VM_SSH_KEYS="ssh-ed25519 ..., ssh-ed25519 ..."
export KUBERNETES_VERSION="1.25.1"
export CONTROL_PLANE_ENDPOINT_IP=10.10.10.4
Expand Down
12 changes: 12 additions & 0 deletions internal/service/vmservice/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ func getMachineAddresses(scope *scope.MachineScope) ([]clusterv1.MachineAddress,
}

func createVM(ctx context.Context, scope *scope.MachineScope) (proxmox.VMCloneResponse, error) {
// TODO: If we dynamically detect the Node, we can't rely on GetNode() here
options := proxmox.VMCloneRequest{
Node: scope.ProxmoxMachine.GetNode(),
// NewID: 0, no need to provide newID
Expand Down Expand Up @@ -371,6 +372,17 @@ func createVM(ctx context.Context, scope *scope.MachineScope) (proxmox.VMCloneRe
}

templateID := scope.ProxmoxMachine.GetTemplateID()
if templateID == -1 {
var err error

templateSelectorTags := scope.ProxmoxMachine.GetTemplateSelectorTags()
options.Node, templateID, err = scope.InfraCluster.ProxmoxClient.FindVMTemplateByTags(ctx, templateSelectorTags)

if err != nil {
scope.SetFailureMessage(err)
return proxmox.VMCloneResponse{}, err
}
}
res, err := scope.InfraCluster.ProxmoxClient.CloneVM(ctx, int(templateID), options)
if err != nil {
return res, err
Expand Down
32 changes: 32 additions & 0 deletions internal/webhook/proxmoxmachine_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ func (p *ProxmoxMachine) ValidateCreate(_ context.Context, obj runtime.Object) (
return warnings, err
}

err = validateTemplate(machine)
if err != nil {
warnings = append(warnings, fmt.Sprintf("cannot create proxmox machine %s", machine.GetName()))
return warnings, err
}

return warnings, nil
}

Expand All @@ -83,6 +89,32 @@ func (p *ProxmoxMachine) ValidateDelete(_ context.Context, _ runtime.Object) (wa
return nil, nil
}

func validateTemplate(machine *infrav1.ProxmoxMachine) error {
gk, name := machine.GroupVersionKind().GroupKind(), machine.GetName()

if (machine.Spec.TemplateID != nil || machine.Spec.SourceNode != "") && (machine.Spec.TemplateSelector != nil) {
return apierrors.NewInvalid(
gk,
name,
field.ErrorList{
field.Invalid(
field.NewPath("spec"), machine.Spec, "spec.sourceNode AND spec.templateID can not be used in combination with spec.templateSelector"),
})
}

if (machine.Spec.TemplateID == nil || machine.Spec.SourceNode == "") && (machine.Spec.TemplateSelector == nil) {
return apierrors.NewInvalid(
gk,
name,
field.ErrorList{
field.Invalid(
field.NewPath("spec"), machine.Spec, "must define either spec.sourceNode AND spec.templateID, or spec.templateSelector"),
})
}

return nil
}

func validateNetworks(machine *infrav1.ProxmoxMachine) error {
if machine.Spec.Network == nil {
return nil
Expand Down
1 change: 1 addition & 0 deletions internal/webhook/proxmoxmachine_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ func validProxmoxMachine(name string) infrav1.ProxmoxMachine {
Spec: infrav1.ProxmoxMachineSpec{
VirtualMachineCloneSpec: infrav1.VirtualMachineCloneSpec{
SourceNode: "pve",
TemplateID: "1337",

Check failure on line 114 in internal/webhook/proxmoxmachine_webhook_test.go

View workflow job for this annotation

GitHub Actions / lint

cannot use "1337" (untyped string constant) as *int32 value in struct literal (typecheck)
},
NumSockets: 1,
NumCores: 1,
Expand Down
1 change: 1 addition & 0 deletions pkg/proxmox/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Client interface {
ConfigureVM(ctx context.Context, vm *proxmox.VirtualMachine, options ...VirtualMachineOption) (*proxmox.Task, error)

FindVMResource(ctx context.Context, vmID uint64) (*proxmox.ClusterResource, error)
FindVMTemplateByTags(ctx context.Context, templateTags []string) (string, int32, error)

GetVM(ctx context.Context, nodeName string, vmID int64) (*proxmox.VirtualMachine, error)

Expand Down
39 changes: 39 additions & 0 deletions pkg/proxmox/goproxmox/api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"context"
"fmt"
"net/url"
"slices"
"strings"

"github.com/go-logr/logr"
Expand Down Expand Up @@ -141,6 +142,44 @@ func (c *APIClient) FindVMResource(ctx context.Context, vmID uint64) (*proxmox.C
return nil, fmt.Errorf("unable to find VM with ID %d on any of the nodes", vmID)
}

// FindTemplateByTags tries to find a VMID by its tags across the whole cluster.

Check failure on line 145 in pkg/proxmox/goproxmox/api_client.go

View workflow job for this annotation

GitHub Actions / lint

exported: comment on exported method APIClient.FindVMTemplateByTags should be of the form "FindVMTemplateByTags ..." (revive)
func (c *APIClient) FindVMTemplateByTags(ctx context.Context, templateTags []string) (string, int32, error) {
vmTemplates := make([]*proxmox.ClusterResource, 0)
slices.Sort(templateTags)

cluster, err := c.Cluster(ctx)
if err != nil {
return "", -1, fmt.Errorf("cannot get cluster status: %w", err)
}

vmResources, err := cluster.Resources(ctx, "vm")
if err != nil {
return "", -1, fmt.Errorf("could not list vm resources: %w", err)
}

for _, vm := range vmResources {
if vm.Template == 0 {
continue
}
if len(vm.Tags) == 0 {
continue
}

vmTags := strings.Split(vm.Tags, ";")
slices.Sort(vmTags)

if slices.Equal(vmTags, templateTags) {
vmTemplates = append(vmTemplates, vm)
}
}

if n := len(vmTemplates); n != 1 {
return "", -1, fmt.Errorf("found %d VM templates with tags %q", n, templateTags)
}

return vmTemplates[0].Node, int32(vmTemplates[0].VMID), nil
}

// DeleteVM deletes a VM based on the nodeName and vmID.
func (c *APIClient) DeleteVM(ctx context.Context, nodeName string, vmID int64) (*proxmox.Task, error) {
// A vmID can not be lower than 100.
Expand Down
Loading

0 comments on commit be8cc1d

Please sign in to comment.