package cpu import ( "context" "fmt" "reflect" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/bpg/terraform-provider-proxmox/proxmox/nodes/vms" proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types" ) var ( _ basetypes.ObjectTypable = Type{} _ basetypes.ObjectValuable = Value{} ) // Type is an attribute type that represents CPU settings. type Type struct { basetypes.ObjectType } // String returns a human-readable representation of the type. func (t Type) String() string { return "cpu.Type" } // ValueFromObject returns a Value given a basetypes.ObjectValue. func (t Type) ValueFromObject( _ context.Context, in basetypes.ObjectValue, ) (basetypes.ObjectValuable, diag.Diagnostics) { value := Value{ Object: in, } return value, nil } // ValueFromTerraform returns a Value given a tftypes.Value. // Value embeds the types.Object value returned from calling ValueFromTerraform on the // types.ObjectType embedded in Type. func (t Type) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { val, err := t.ObjectType.ValueFromTerraform(ctx, in) if err != nil { return nil, fmt.Errorf("failed to convert value to types.Object: %w", err) } obj, ok := val.(types.Object) if !ok { return nil, fmt.Errorf("%T cannot be used as types.Object", val) } return Value{obj}, nil } // ValueType returns the associated Value type for debugging. func (t Type) ValueType(context.Context) attr.Value { // It does not need to be a fully valid implementation of the type. return Value{} } // Equal returns true if `candidate` is also a Type and has the same // AttributeTypes. func (t Type) Equal(candidate attr.Type) bool { other, ok := candidate.(Type) if !ok { return false } return t.ObjectType.Equal(other.ObjectType) } // Value represents an object containing values to be used as CPU settings. type Value struct { types.Object } // Equal returns true if the Value is considered semantically equal // (same type and same value) to the attr.Value passed as an argument. func (v Value) Equal(c attr.Value) bool { other, ok := c.(Value) if !ok { return false } return v.Object.Equal(other.Object) } // ToObjectValue returns the underlying ObjectValue. func (v Value) ToObjectValue(_ context.Context) (basetypes.ObjectValue, diag.Diagnostics) { return v.Object, nil } // Type returns a Type with the same attribute types as `t`. func (v Value) Type(ctx context.Context) attr.Type { return Type{ types.ObjectType{ AttrTypes: v.AttributeTypes(ctx), }, } } // NewValue returns a new Value with the given CPU settings from the PVE API. func NewValue(ctx context.Context, config *vms.GetResponseData, diags *diag.Diagnostics) Value { cpu := Model{} cpu.Affinity = types.StringPointerValue(config.CPUAffinity) cpu.Architecture = types.StringPointerValue(config.CPUArchitecture) cpu.Hotplugged = types.Int64PointerValue(config.VirtualCPUCount) cpu.Limit = types.Int64PointerValue(config.CPULimit.PointerInt64()) cpu.Numa = types.BoolPointerValue(config.NUMAEnabled.PointerBool()) cpu.Units = types.Int64PointerValue(config.CPUUnits) // special cases: PVE does not return actual value for cores VM, etc is using default (i.e. a value is not specified) if config.CPUCores != nil { cpu.Cores = types.Int64PointerValue(config.CPUCores) } else { cpu.Cores = types.Int64Value(1) } if config.CPUSockets != nil { cpu.Sockets = types.Int64PointerValue(config.CPUSockets) } else { cpu.Sockets = types.Int64Value(1) } if config.CPUEmulation != nil { cpu.Type = types.StringValue(config.CPUEmulation.Type) flags, d := types.SetValueFrom(ctx, basetypes.StringType{}, config.CPUEmulation.Flags) diags.Append(d...) cpu.Flags = flags } else { cpu.Type = types.StringValue("kvm64") cpu.Flags = types.SetNull(basetypes.StringType{}) } obj, d := types.ObjectValueFrom(ctx, attributeTypes(), cpu) diags.Append(d...) return Value{obj} } // FillCreateBody fills the CreateRequestBody with the CPU settings from the Value. // // In the 'create' context, v is the plan. func (v Value) FillCreateBody(ctx context.Context, body *vms.CreateRequestBody, diags *diag.Diagnostics) { var plan Model if v.IsNull() || v.IsUnknown() { return } d := v.Object.As(ctx, &plan, basetypes.ObjectAsOptions{}) diags.Append(d...) if d.HasError() { return } // for computed fields, we need to check if they are unknown if !plan.Affinity.IsUnknown() { body.CPUAffinity = plan.Affinity.ValueStringPointer() } if !plan.Architecture.IsUnknown() { body.CPUArchitecture = plan.Architecture.ValueStringPointer() } if !plan.Cores.IsUnknown() { body.CPUCores = plan.Cores.ValueInt64Pointer() } if !plan.Limit.IsUnknown() { body.CPULimit = plan.Limit.ValueInt64Pointer() } if !plan.Sockets.IsUnknown() { body.CPUSockets = plan.Sockets.ValueInt64Pointer() } if !plan.Units.IsUnknown() { body.CPUUnits = plan.Units.ValueInt64Pointer() } if !plan.Numa.IsUnknown() { body.NUMAEnabled = proxmoxtypes.CustomBoolPtr(plan.Numa.ValueBoolPointer()) } if !plan.Hotplugged.IsUnknown() { body.VirtualCPUCount = plan.Hotplugged.ValueInt64Pointer() } body.CPUEmulation = &vms.CustomCPUEmulation{} if !plan.Type.IsUnknown() { body.CPUEmulation.Type = plan.Type.ValueString() } if !plan.Flags.IsUnknown() { d = plan.Flags.ElementsAs(ctx, &body.CPUEmulation.Flags, false) diags.Append(d...) } } // FillUpdateBody fills the UpdateRequestBody with the CPU settings from the Value. // // In the 'update' context, v is the plan and stateValue is the current state. func (v Value) FillUpdateBody( ctx context.Context, stateValue Value, updateBody *vms.UpdateRequestBody, isClone bool, diags *diag.Diagnostics, ) { var plan, state Model if v.IsNull() || v.IsUnknown() || v.Equal(stateValue) { return } d := v.Object.As(ctx, &plan, basetypes.ObjectAsOptions{}) diags.Append(d...) d = stateValue.Object.As(ctx, &state, basetypes.ObjectAsOptions{}) diags.Append(d...) if diags.HasError() { return } var errs []error del := func(field string) { errs = append(errs, updateBody.ToDelete(field)) } if !plan.Affinity.Equal(state.Affinity) { if shouldBeRemoved(plan.Affinity, state.Affinity, isClone) { del("CPUAffinity") } else if isDefined(plan.Affinity) { updateBody.CPUAffinity = plan.Affinity.ValueStringPointer() } } if !plan.Architecture.Equal(state.Architecture) { if shouldBeRemoved(plan.Architecture, state.Architecture, isClone) { del("CPUArchitecture") } else if isDefined(plan.Architecture) { updateBody.CPUArchitecture = plan.Architecture.ValueStringPointer() } } if !plan.Cores.Equal(state.Cores) { if shouldBeRemoved(plan.Cores, state.Cores, isClone) { del("CPUCores") } else if isDefined(plan.Cores) { updateBody.CPUCores = plan.Cores.ValueInt64Pointer() } } if !plan.Limit.Equal(state.Limit) { if shouldBeRemoved(plan.Limit, state.Limit, isClone) { del("CPULimit") } else if isDefined(plan.Sockets) { updateBody.CPULimit = plan.Limit.ValueInt64Pointer() } } if !plan.Sockets.Equal(state.Sockets) { if shouldBeRemoved(plan.Sockets, state.Sockets, isClone) { del("CPUSockets") } else if isDefined(plan.Sockets) { updateBody.CPUSockets = plan.Sockets.ValueInt64Pointer() } } if !plan.Units.Equal(state.Units) { if shouldBeRemoved(plan.Units, state.Units, isClone) { del("CPUUnits") } else if isDefined(plan.Units) { updateBody.CPUUnits = plan.Units.ValueInt64Pointer() } } if !plan.Numa.Equal(state.Numa) { if shouldBeRemoved(plan.Numa, state.Numa, isClone) { del("NUMAEnabled") } else if isDefined(plan.Numa) { updateBody.NUMAEnabled = proxmoxtypes.CustomBoolPtr(plan.Numa.ValueBoolPointer()) } } if !plan.Hotplugged.Equal(state.Hotplugged) { if shouldBeRemoved(plan.Hotplugged, state.Hotplugged, isClone) { del("VirtualCPUCount") } else if isDefined(plan.Hotplugged) { updateBody.VirtualCPUCount = plan.Hotplugged.ValueInt64Pointer() } } var delType, delFlags bool cpuEmulation := &vms.CustomCPUEmulation{} if !plan.Type.Equal(state.Type) { if shouldBeRemoved(plan.Type, state.Type, isClone) { delType = true } else if isDefined(plan.Type) { cpuEmulation.Type = plan.Type.ValueString() } } if !plan.Flags.Equal(state.Flags) { if shouldBeRemoved(plan.Flags, state.Flags, isClone) { delFlags = true } else if isDefined(plan.Flags) { d = plan.Flags.ElementsAs(ctx, &cpuEmulation.Flags, false) diags.Append(d...) } } switch { case delType && !delFlags: diags.AddError("Cannot have CPU flags without explicit definition of CPU type", "") case delType: del("CPUEmulation") case !reflect.DeepEqual(cpuEmulation, &vms.CustomCPUEmulation{}): updateBody.CPUEmulation = cpuEmulation } } func shouldBeRemoved(plan attr.Value, state attr.Value, isClone bool) bool { return !isDefined(plan) && isDefined(state) && !isClone } func isDefined(v attr.Value) bool { return !v.IsNull() && !v.IsUnknown() }