diff --git a/api/v1beta1/types.go b/api/v1beta1/types.go
index 5cb066219e..09ff2f7e1b 100644
--- a/api/v1beta1/types.go
+++ b/api/v1beta1/types.go
@@ -290,6 +290,32 @@ type AllocationPool struct {
End string `json:"end"`
}
+// QoSPolicyParam specifies an OpenStack QoS Policy to use. It requires the neutron qos extension to be enabled.
+// It may be specified by either ID or filter, but not both.
+// +kubebuilder:validation:MaxProperties:=1
+// +kubebuilder:validation:MinProperties:=1
+type QoSPolicyParam struct {
+ // ID is the ID of the QoS policy to use. If ID is provided, filter cannot be provided. Must be in UUID format.
+ // +kubebuilder:validation:Format:=uuid
+ // +optional
+ ID optional.String `json:"id,omitempty"`
+
+ // Filter specifies a filter to select an OpenStack QoS policy. If provided, cannot be empty.
+ Filter *QoSPolicyFilter `json:"filter,omitempty"`
+}
+
+// QoSPolicyFilter specifies a query to select an OpenStack QoS Policy. At least one property must be set.
+// +kubebuilder:validation:MinProperties:=1
+type QoSPolicyFilter struct {
+ Name string `json:"name,omitempty"`
+ Description string `json:"description,omitempty"`
+ ProjectID string `json:"projectID,omitempty"`
+ Shared *bool `json:"shared,omitempty"`
+ IsDefault *bool `json:"isDefault,omitempty"`
+
+ FilterByNeutronTags `json:",inline"`
+}
+
type PortOpts struct {
// Network is a query for an openstack network that the port will be created or discovered on.
// This will fail if the query returns more than one network.
@@ -326,6 +352,11 @@ type PortOpts struct {
// +optional
Trunk *bool `json:"trunk,omitempty"`
+ // QoSPolicy is a query for an openstack QoS policy that the port will use.
+ // This will fail if the query returns more than one qos policy.
+ // +optional
+ QoSPolicy *QoSPolicyParam `json:"qosPolicy,omitempty"`
+
ResolvedPortSpecFields `json:",inline"`
}
@@ -422,6 +453,10 @@ type ResolvedPortSpec struct {
// +listType=atomic
SecurityGroups []string `json:"securityGroups,omitempty"`
+ // QoSPolicyID is the ID of the qos policy the port will use.
+ // +optional
+ QoSPolicyID *string `json:"qosPolicyID,omitempty"`
+
ResolvedPortSpecFields `json:",inline"`
}
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index b93422b7a3..4489059d73 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -1281,6 +1281,11 @@ func (in *PortOpts) DeepCopyInto(out *PortOpts) {
*out = new(bool)
**out = **in
}
+ if in.QoSPolicy != nil {
+ in, out := &in.QoSPolicy, &out.QoSPolicy
+ *out = new(QoSPolicyParam)
+ (*in).DeepCopyInto(*out)
+ }
in.ResolvedPortSpecFields.DeepCopyInto(&out.ResolvedPortSpecFields)
}
@@ -1309,6 +1314,57 @@ func (in *PortStatus) DeepCopy() *PortStatus {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *QoSPolicyFilter) DeepCopyInto(out *QoSPolicyFilter) {
+ *out = *in
+ if in.Shared != nil {
+ in, out := &in.Shared, &out.Shared
+ *out = new(bool)
+ **out = **in
+ }
+ if in.IsDefault != nil {
+ in, out := &in.IsDefault, &out.IsDefault
+ *out = new(bool)
+ **out = **in
+ }
+ in.FilterByNeutronTags.DeepCopyInto(&out.FilterByNeutronTags)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QoSPolicyFilter.
+func (in *QoSPolicyFilter) DeepCopy() *QoSPolicyFilter {
+ if in == nil {
+ return nil
+ }
+ out := new(QoSPolicyFilter)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *QoSPolicyParam) DeepCopyInto(out *QoSPolicyParam) {
+ *out = *in
+ if in.ID != nil {
+ in, out := &in.ID, &out.ID
+ *out = new(string)
+ **out = **in
+ }
+ if in.Filter != nil {
+ in, out := &in.Filter, &out.Filter
+ *out = new(QoSPolicyFilter)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QoSPolicyParam.
+func (in *QoSPolicyParam) DeepCopy() *QoSPolicyParam {
+ if in == nil {
+ return nil
+ }
+ out := new(QoSPolicyParam)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResolvedFixedIP) DeepCopyInto(out *ResolvedFixedIP) {
*out = *in
@@ -1381,6 +1437,11 @@ func (in *ResolvedPortSpec) DeepCopyInto(out *ResolvedPortSpec) {
*out = make([]string, len(*in))
copy(*out, *in)
}
+ if in.QoSPolicyID != nil {
+ in, out := &in.QoSPolicyID, &out.QoSPolicyID
+ *out = new(string)
+ **out = **in
+ }
in.ResolvedPortSpecFields.DeepCopyInto(&out.ResolvedPortSpecFields)
}
diff --git a/api_violations.report b/api_violations.report
index baf94dfeec..0bf78d3aa0 100644
--- a/api_violations.report
+++ b/api_violations.report
@@ -82,7 +82,9 @@ API rule violation: names_match,k8s.io/apimachinery/pkg/apis/meta/v1,StatusCause
API rule violation: names_match,k8s.io/apimachinery/pkg/apis/meta/v1,Time,Time
API rule violation: names_match,k8s.io/apimachinery/pkg/runtime,Unknown,ContentEncoding
API rule violation: names_match,k8s.io/apimachinery/pkg/runtime,Unknown,ContentType
+API rule violation: names_match,sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1,PortOpts,QoSPolicy
API rule violation: names_match,sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1,ResolvedFixedIP,SubnetID
+API rule violation: names_match,sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1,ResolvedPortSpec,QoSPolicyID
API rule violation: names_match,sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1,Router,IPs
API rule violation: names_match,sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1,SubnetFilter,IPv6AddressMode
API rule violation: names_match,sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1,SubnetFilter,IPv6RAMode
diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go
index a6c2ea4334..d41112b22c 100644
--- a/cmd/models-schema/zz_generated.openapi.go
+++ b/cmd/models-schema/zz_generated.openapi.go
@@ -370,6 +370,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.OpenStackMachineTemplateSpec": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_OpenStackMachineTemplateSpec(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.PortOpts": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_PortOpts(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.PortStatus": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_PortStatus(ref),
+ "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.QoSPolicyFilter": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_QoSPolicyFilter(ref),
+ "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.QoSPolicyParam": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_QoSPolicyParam(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.ResolvedFixedIP": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_ResolvedFixedIP(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.ResolvedMachineSpec": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_ResolvedMachineSpec(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.ResolvedPortSpec": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_ResolvedPortSpec(ref),
@@ -19924,6 +19926,12 @@ func schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_PortOpts(ref co
Format: "",
},
},
+ "qosPolicy": {
+ SchemaProps: spec.SchemaProps{
+ Description: "QoSPolicy is a query for an openstack QoS policy that the port will use. This will fail if the query returns more than one qos policy.",
+ Ref: ref("sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.QoSPolicyParam"),
+ },
+ },
"adminStateUp": {
SchemaProps: spec.SchemaProps{
Description: "AdminStateUp specifies whether the port should be created in the up (true) or down (false) state. The default is up.",
@@ -20012,7 +20020,7 @@ func schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_PortOpts(ref co
},
},
Dependencies: []string{
- "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.AddressPair", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.BindingProfile", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.FixedIP", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.NetworkParam", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.SecurityGroupParam", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.ValueSpec"},
+ "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.AddressPair", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.BindingProfile", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.FixedIP", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.NetworkParam", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.QoSPolicyParam", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.SecurityGroupParam", "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.ValueSpec"},
}
}
@@ -20037,6 +20045,157 @@ func schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_PortStatus(ref
}
}
+func schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_QoSPolicyFilter(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "QoSPolicyFilter specifies a query to select an OpenStack QoS Policy. At least one property must be set.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "name": {
+ SchemaProps: spec.SchemaProps{
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "description": {
+ SchemaProps: spec.SchemaProps{
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "projectID": {
+ SchemaProps: spec.SchemaProps{
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "shared": {
+ SchemaProps: spec.SchemaProps{
+ Type: []string{"boolean"},
+ Format: "",
+ },
+ },
+ "isDefault": {
+ SchemaProps: spec.SchemaProps{
+ Type: []string{"boolean"},
+ Format: "",
+ },
+ },
+ "tags": {
+ VendorExtensible: spec.VendorExtensible{
+ Extensions: spec.Extensions{
+ "x-kubernetes-list-type": "set",
+ },
+ },
+ SchemaProps: spec.SchemaProps{
+ Description: "Tags is a list of tags to filter by. If specified, the resource must have all of the tags specified to be included in the result.",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: "",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ },
+ },
+ "tagsAny": {
+ VendorExtensible: spec.VendorExtensible{
+ Extensions: spec.Extensions{
+ "x-kubernetes-list-type": "set",
+ },
+ },
+ SchemaProps: spec.SchemaProps{
+ Description: "TagsAny is a list of tags to filter by. If specified, the resource must have at least one of the tags specified to be included in the result.",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: "",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ },
+ },
+ "notTags": {
+ VendorExtensible: spec.VendorExtensible{
+ Extensions: spec.Extensions{
+ "x-kubernetes-list-type": "set",
+ },
+ },
+ SchemaProps: spec.SchemaProps{
+ Description: "NotTags is a list of tags to filter by. If specified, resources which contain all of the given tags will be excluded from the result.",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: "",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ },
+ },
+ "notTagsAny": {
+ VendorExtensible: spec.VendorExtensible{
+ Extensions: spec.Extensions{
+ "x-kubernetes-list-type": "set",
+ },
+ },
+ SchemaProps: spec.SchemaProps{
+ Description: "NotTagsAny is a list of tags to filter by. If specified, resources which contain any of the given tags will be excluded from the result.",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: "",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_QoSPolicyParam(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "QoSPolicyParam specifies an OpenStack QoS Policy to use. It requires the neutron qos extension to be enabled. It may be specified by either ID or filter, but not both.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "id": {
+ SchemaProps: spec.SchemaProps{
+ Description: "ID is the ID of the QoS policy to use. If ID is provided, filter cannot be provided. Must be in UUID format.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "filter": {
+ SchemaProps: spec.SchemaProps{
+ Description: "Filter specifies a filter to select an OpenStack QoS policy. If provided, cannot be empty.",
+ Ref: ref("sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.QoSPolicyFilter"),
+ },
+ },
+ },
+ },
+ },
+ Dependencies: []string{
+ "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.QoSPolicyFilter"},
+ }
+}
+
func schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_ResolvedFixedIP(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -20211,6 +20370,13 @@ func schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_ResolvedPortSpe
},
},
},
+ "qosPolicyID": {
+ SchemaProps: spec.SchemaProps{
+ Description: "QoSPolicyID is the ID of the qos policy the port will use.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
"adminStateUp": {
SchemaProps: spec.SchemaProps{
Description: "AdminStateUp specifies whether the port should be created in the up (true) or down (false) state. The default is up.",
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml
index 4cf9b3daec..51be234726 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml
@@ -854,6 +854,90 @@ spec:
description: PropageteUplinkStatus enables or disables
the propagate uplink status on the port.
type: boolean
+ qosPolicy:
+ description: |-
+ QoSPolicy is a query for an openstack QoS policy that the port will use.
+ This will fail if the query returns more than one qos policy.
+ maxProperties: 1
+ minProperties: 1
+ properties:
+ filter:
+ description: Filter specifies a filter to select
+ an OpenStack QoS policy. If provided, cannot be
+ empty.
+ minProperties: 1
+ properties:
+ description:
+ type: string
+ isDefault:
+ type: boolean
+ name:
+ type: string
+ notTags:
+ description: |-
+ NotTags is a list of tags to filter by. If specified, resources which
+ contain all of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ notTagsAny:
+ description: |-
+ NotTagsAny is a list of tags to filter by. If specified, resources
+ which contain any of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ projectID:
+ type: string
+ shared:
+ type: boolean
+ tags:
+ description: |-
+ Tags is a list of tags to filter by. If specified, the resource must
+ have all of the tags specified to be included in the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ tagsAny:
+ description: |-
+ TagsAny is a list of tags to filter by. If specified, the resource
+ must have at least one of the tags specified to be included in the
+ result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ type: object
+ id:
+ description: ID is the ID of the QoS policy to use.
+ If ID is provided, filter cannot be provided.
+ Must be in UUID format.
+ format: uuid
+ type: string
+ type: object
securityGroups:
description: SecurityGroups is a list of the names,
uuids, filters or any combination these of the security
@@ -2352,6 +2436,10 @@ spec:
description: PropageteUplinkStatus enables or disables
the propagate uplink status on the port.
type: boolean
+ qosPolicyID:
+ description: QoSPolicyID is the ID of the qos policy
+ the port will use.
+ type: string
securityGroups:
description: SecurityGroups is a list of security group
IDs to assign to the port.
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml
index 084846a638..072ef35bac 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml
@@ -849,6 +849,90 @@ spec:
disables the propagate uplink status on the
port.
type: boolean
+ qosPolicy:
+ description: |-
+ QoSPolicy is a query for an openstack QoS policy that the port will use.
+ This will fail if the query returns more than one qos policy.
+ maxProperties: 1
+ minProperties: 1
+ properties:
+ filter:
+ description: Filter specifies a filter to
+ select an OpenStack QoS policy. If provided,
+ cannot be empty.
+ minProperties: 1
+ properties:
+ description:
+ type: string
+ isDefault:
+ type: boolean
+ name:
+ type: string
+ notTags:
+ description: |-
+ NotTags is a list of tags to filter by. If specified, resources which
+ contain all of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ notTagsAny:
+ description: |-
+ NotTagsAny is a list of tags to filter by. If specified, resources
+ which contain any of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ projectID:
+ type: string
+ shared:
+ type: boolean
+ tags:
+ description: |-
+ Tags is a list of tags to filter by. If specified, the resource must
+ have all of the tags specified to be included in the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ tagsAny:
+ description: |-
+ TagsAny is a list of tags to filter by. If specified, the resource
+ must have at least one of the tags specified to be included in the
+ result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ type: object
+ id:
+ description: ID is the ID of the QoS policy
+ to use. If ID is provided, filter cannot
+ be provided. Must be in UUID format.
+ format: uuid
+ type: string
+ type: object
securityGroups:
description: SecurityGroups is a list of the
names, uuids, filters or any combination these
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachines.yaml
index 0dc88ef8c7..84123622fa 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachines.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachines.yaml
@@ -538,6 +538,89 @@ spec:
description: PropageteUplinkStatus enables or disables the propagate
uplink status on the port.
type: boolean
+ qosPolicy:
+ description: |-
+ QoSPolicy is a query for an openstack QoS policy that the port will use.
+ This will fail if the query returns more than one qos policy.
+ maxProperties: 1
+ minProperties: 1
+ properties:
+ filter:
+ description: Filter specifies a filter to select an OpenStack
+ QoS policy. If provided, cannot be empty.
+ minProperties: 1
+ properties:
+ description:
+ type: string
+ isDefault:
+ type: boolean
+ name:
+ type: string
+ notTags:
+ description: |-
+ NotTags is a list of tags to filter by. If specified, resources which
+ contain all of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ notTagsAny:
+ description: |-
+ NotTagsAny is a list of tags to filter by. If specified, resources
+ which contain any of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ projectID:
+ type: string
+ shared:
+ type: boolean
+ tags:
+ description: |-
+ Tags is a list of tags to filter by. If specified, the resource must
+ have all of the tags specified to be included in the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ tagsAny:
+ description: |-
+ TagsAny is a list of tags to filter by. If specified, the resource
+ must have at least one of the tags specified to be included in the
+ result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ type: object
+ id:
+ description: ID is the ID of the QoS policy to use. If ID
+ is provided, filter cannot be provided. Must be in UUID
+ format.
+ format: uuid
+ type: string
+ type: object
securityGroups:
description: SecurityGroups is a list of the names, uuids, filters
or any combination these of the security groups to assign
@@ -1173,6 +1256,10 @@ spec:
description: PropageteUplinkStatus enables or disables the
propagate uplink status on the port.
type: boolean
+ qosPolicyID:
+ description: QoSPolicyID is the ID of the qos policy the
+ port will use.
+ type: string
securityGroups:
description: SecurityGroups is a list of security group
IDs to assign to the port.
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachinetemplates.yaml
index e57463f1c3..8cd8a35620 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachinetemplates.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachinetemplates.yaml
@@ -532,6 +532,90 @@ spec:
description: PropageteUplinkStatus enables or disables
the propagate uplink status on the port.
type: boolean
+ qosPolicy:
+ description: |-
+ QoSPolicy is a query for an openstack QoS policy that the port will use.
+ This will fail if the query returns more than one qos policy.
+ maxProperties: 1
+ minProperties: 1
+ properties:
+ filter:
+ description: Filter specifies a filter to select
+ an OpenStack QoS policy. If provided, cannot be
+ empty.
+ minProperties: 1
+ properties:
+ description:
+ type: string
+ isDefault:
+ type: boolean
+ name:
+ type: string
+ notTags:
+ description: |-
+ NotTags is a list of tags to filter by. If specified, resources which
+ contain all of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ notTagsAny:
+ description: |-
+ NotTagsAny is a list of tags to filter by. If specified, resources
+ which contain any of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ projectID:
+ type: string
+ shared:
+ type: boolean
+ tags:
+ description: |-
+ Tags is a list of tags to filter by. If specified, the resource must
+ have all of the tags specified to be included in the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ tagsAny:
+ description: |-
+ TagsAny is a list of tags to filter by. If specified, the resource
+ must have at least one of the tags specified to be included in the
+ result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ type: object
+ id:
+ description: ID is the ID of the QoS policy to use.
+ If ID is provided, filter cannot be provided.
+ Must be in UUID format.
+ format: uuid
+ type: string
+ type: object
securityGroups:
description: SecurityGroups is a list of the names,
uuids, filters or any combination these of the security
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackservers.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackservers.yaml
index 31de37e0a9..9816806d3f 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackservers.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackservers.yaml
@@ -531,6 +531,89 @@ spec:
description: PropageteUplinkStatus enables or disables the propagate
uplink status on the port.
type: boolean
+ qosPolicy:
+ description: |-
+ QoSPolicy is a query for an openstack QoS policy that the port will use.
+ This will fail if the query returns more than one qos policy.
+ maxProperties: 1
+ minProperties: 1
+ properties:
+ filter:
+ description: Filter specifies a filter to select an OpenStack
+ QoS policy. If provided, cannot be empty.
+ minProperties: 1
+ properties:
+ description:
+ type: string
+ isDefault:
+ type: boolean
+ name:
+ type: string
+ notTags:
+ description: |-
+ NotTags is a list of tags to filter by. If specified, resources which
+ contain all of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ notTagsAny:
+ description: |-
+ NotTagsAny is a list of tags to filter by. If specified, resources
+ which contain any of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ projectID:
+ type: string
+ shared:
+ type: boolean
+ tags:
+ description: |-
+ Tags is a list of tags to filter by. If specified, the resource must
+ have all of the tags specified to be included in the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ tagsAny:
+ description: |-
+ TagsAny is a list of tags to filter by. If specified, the resource
+ must have at least one of the tags specified to be included in the
+ result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ type: object
+ id:
+ description: ID is the ID of the QoS policy to use. If ID
+ is provided, filter cannot be provided. Must be in UUID
+ format.
+ format: uuid
+ type: string
+ type: object
securityGroups:
description: SecurityGroups is a list of the names, uuids, filters
or any combination these of the security groups to assign
@@ -1158,6 +1241,10 @@ spec:
description: PropageteUplinkStatus enables or disables the
propagate uplink status on the port.
type: boolean
+ qosPolicyID:
+ description: QoSPolicyID is the ID of the qos policy the
+ port will use.
+ type: string
securityGroups:
description: SecurityGroups is a list of security group
IDs to assign to the port.
diff --git a/controllers/openstackserver_controller.go b/controllers/openstackserver_controller.go
index 26494bfa49..f8f5e9887c 100644
--- a/controllers/openstackserver_controller.go
+++ b/controllers/openstackserver_controller.go
@@ -58,6 +58,7 @@ import (
"sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/networking"
"sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
capoerrors "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors"
+ "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/extensions"
"sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/names"
)
@@ -263,7 +264,7 @@ func (r *OpenStackServerReconciler) reconcileDelete(scope *scope.WithLogger, ope
}
}
- trunkSupported, err := networkingService.IsTrunkExtSupported()
+ trunkSupported, err := networkingService.IsExtensionSupported(extensions.TrunkExtensionName)
if err != nil {
return err
}
diff --git a/controllers/openstackserver_controller_test.go b/controllers/openstackserver_controller_test.go
index 253264fb26..6552893d2c 100644
--- a/controllers/openstackserver_controller_test.go
+++ b/controllers/openstackserver_controller_test.go
@@ -42,6 +42,7 @@ import (
"sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock"
"sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/compute"
"sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
+ capoextensions "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/extensions"
)
const (
@@ -148,7 +149,7 @@ var listDefaultServerFound = func(r *recorders) {
var deleteDefaultPorts = func(r *recorders) {
trunkExtension := extensions.Extension{}
- trunkExtension.Alias = "trunk"
+ trunkExtension.Alias = capoextensions.TrunkExtensionName
r.network.ListExtensions().Return([]extensions.Extension{trunkExtension}, nil)
r.network.ListTrunk(trunks.ListOpts{PortID: portUUID}).Return([]trunks.Trunk{{ID: trunkUUID}}, nil)
r.network.ListTrunkSubports(trunkUUID).Return([]trunks.Subport{}, nil)
diff --git a/docs/book/src/api/v1beta1/api.md b/docs/book/src/api/v1beta1/api.md
index 0eb377d117..a69050191a 100644
--- a/docs/book/src/api/v1beta1/api.md
+++ b/docs/book/src/api/v1beta1/api.md
@@ -1612,6 +1612,7 @@ SubnetParam
(Appears on:
NetworkFilter,
+QoSPolicyFilter,
RouterFilter,
SecurityGroupFilter,
SubnetFilter)
@@ -4115,6 +4116,21 @@ bastion host.
+qosPolicy
+
+
+QoSPolicyParam
+
+
+ |
+
+(Optional)
+ QoSPolicy is a query for an openstack QoS policy that the port will use.
+This will fail if the query returns more than one qos policy.
+ |
+
+
+
ResolvedPortSpecFields
@@ -4159,6 +4175,135 @@ string
|
+QoSPolicyFilter
+
+
+(Appears on:
+QoSPolicyParam)
+
+
+
QoSPolicyFilter specifies a query to select an OpenStack QoS Policy. At least one property must be set.
+
+
+
+
+| Field |
+Description |
+
+
+
+
+
+name
+
+string
+
+ |
+
+ |
+
+
+
+description
+
+string
+
+ |
+
+ |
+
+
+
+projectID
+
+string
+
+ |
+
+ |
+
+
+
+shared
+
+bool
+
+ |
+
+ |
+
+
+
+isDefault
+
+bool
+
+ |
+
+ |
+
+
+
+FilterByNeutronTags
+
+
+FilterByNeutronTags
+
+
+ |
+
+
+(Members of FilterByNeutronTags are embedded into this type.)
+
+ |
+
+
+
+QoSPolicyParam
+
+
+(Appears on:
+PortOpts)
+
+
+
QoSPolicyParam specifies an OpenStack QoS Policy to use. It requires the neutron qos extension to be enabled.
+It may be specified by either ID or filter, but not both.
+
+
+
+
+| Field |
+Description |
+
+
+
+
+
+id
+
+string
+
+ |
+
+(Optional)
+ ID is the ID of the QoS policy to use. If ID is provided, filter cannot be provided. Must be in UUID format.
+ |
+
+
+
+filter
+
+
+QoSPolicyFilter
+
+
+ |
+
+ Filter specifies a filter to select an OpenStack QoS policy. If provided, cannot be empty.
+ |
+
+
+
ResolvedFixedIP
@@ -4377,6 +4522,18 @@ bool
+qosPolicyID
+
+string
+
+ |
+
+(Optional)
+ QoSPolicyID is the ID of the qos policy the port will use.
+ |
+
+
+
ResolvedPortSpecFields
diff --git a/docs/book/src/clusteropenstack/configuration.md b/docs/book/src/clusteropenstack/configuration.md
index ddd7c5943c..9e29f8083a 100644
--- a/docs/book/src/clusteropenstack/configuration.md
+++ b/docs/book/src/clusteropenstack/configuration.md
@@ -32,6 +32,7 @@
- [Port network and IP addresses](#port-network-and-ip-addresses)
- [Examples](#examples)
- [Port Security](#port-security)
+ - [Port QoS Policy](#port-qos-policy)
- [Security groups](#security-groups)
- [Tagging](#tagging)
- [Metadata](#metadata)
@@ -571,6 +572,52 @@ spec:
...
```
+### Port QoS Policy
+
+Assigning a QoS Policy to a port can be done by specifying the id or a filter. More details about the filter can be found in [QoSPolicyParam](https://github.com/kubernetes-sigs/cluster-api-provider-openstack/blob/main/api/v1beta1/types.go). When using filters to look up a QoS policy, please note that exactly one policy must be returned. The neutron [qos](https://docs.openstack.org/neutron/latest/admin/config-qos.html) extension needs to be enabled on the OpenStack cloud when using this feature.
+
+Example defining QoS policy id:
+
+```yaml
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+kind: OpenStackMachineTemplate
+metadata:
+ name: -controlplane
+ namespace:
+spec:
+ template:
+ spec:
+ ports:
+ - network:
+ id:
+ ...
+ qosPolicy:
+ id:
+ ...
+```
+
+Example defining QoS policy filter:
+
+```yaml
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+kind: OpenStackMachineTemplate
+metadata:
+ name: -controlplane
+ namespace:
+spec:
+ template:
+ spec:
+ ports:
+ - network:
+ id:
+ ...
+ qosPolicy:
+ filter:
+ name:
+ shared: true
+ ...
+```
+
## Security groups
Security groups are used to determine which ports of the cluster nodes are accessible from where.
diff --git a/hack/ci/cloud-init/controller.yaml.tpl b/hack/ci/cloud-init/controller.yaml.tpl
index e89a448571..2e6fcf1f09 100644
--- a/hack/ci/cloud-init/controller.yaml.tpl
+++ b/hack/ci/cloud-init/controller.yaml.tpl
@@ -38,7 +38,7 @@
# Neutron
enable_plugin neutron https://github.com/openstack/neutron stable/${OPENSTACK_RELEASE}
- ENABLED_SERVICES+=,q-svc,neutron-trunk,ovn-controller,ovs-vswitchd,ovn-northd,ovsdb-server,q-ovn-metadata-agent
+ ENABLED_SERVICES+=,q-svc,neutron-trunk,ovn-controller,ovs-vswitchd,ovn-northd,ovsdb-server,q-ovn-metadata-agent,q-qos
DISABLED_SERVICES=q-agt,q-dhcp,q-l3,q-meta,q-metering
PUBLIC_BRIDGE_MTU=${MTU}
@@ -197,6 +197,9 @@
openstack quota set --secgroup-rules 1000 demo
openstack quota set --secgroups 100 admin
openstack quota set --secgroup-rules 1000 admin
+
+ # Validate qos extension is set
+ openstack extension list --network | grep qos
- path: /root/devstack.sh
permissions: "0755"
content: |
diff --git a/pkg/clients/mock/network.go b/pkg/clients/mock/network.go
index 1dfafc4d06..28b468e47b 100644
--- a/pkg/clients/mock/network.go
+++ b/pkg/clients/mock/network.go
@@ -31,6 +31,7 @@ import (
attributestags "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags"
floatingips "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips"
routers "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers"
+ policies "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/qos/policies"
groups "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups"
rules "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules"
trunks "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks"
@@ -38,6 +39,7 @@ import (
ports "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports"
subnets "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets"
gomock "go.uber.org/mock/gomock"
+ clients "sigs.k8s.io/cluster-api-provider-openstack/pkg/clients"
)
// MockNetworkClient is a mock of NetworkClient interface.
@@ -356,6 +358,21 @@ func (mr *MockNetworkClientMockRecorder) GetPort(id any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPort", reflect.TypeOf((*MockNetworkClient)(nil).GetPort), id)
}
+// GetPortWithQoS mocks base method.
+func (m *MockNetworkClient) GetPortWithQoS(id string) (*clients.PortWithQoS, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetPortWithQoS", id)
+ ret0, _ := ret[0].(*clients.PortWithQoS)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetPortWithQoS indicates an expected call of GetPortWithQoS.
+func (mr *MockNetworkClientMockRecorder) GetPortWithQoS(id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPortWithQoS", reflect.TypeOf((*MockNetworkClient)(nil).GetPortWithQoS), id)
+}
+
// GetRouter mocks base method.
func (m *MockNetworkClient) GetRouter(id string) (*routers.Router, error) {
m.ctrl.T.Helper()
@@ -476,6 +493,21 @@ func (mr *MockNetworkClientMockRecorder) ListPort(opts any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPort", reflect.TypeOf((*MockNetworkClient)(nil).ListPort), opts)
}
+// ListQoSPolicy mocks base method.
+func (m *MockNetworkClient) ListQoSPolicy(opts policies.ListOpts) ([]policies.Policy, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListQoSPolicy", opts)
+ ret0, _ := ret[0].([]policies.Policy)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListQoSPolicy indicates an expected call of ListQoSPolicy.
+func (mr *MockNetworkClientMockRecorder) ListQoSPolicy(opts any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListQoSPolicy", reflect.TypeOf((*MockNetworkClient)(nil).ListQoSPolicy), opts)
+}
+
// ListRouter mocks base method.
func (m *MockNetworkClient) ListRouter(opts routers.ListOpts) ([]routers.Router, error) {
m.ctrl.T.Helper()
diff --git a/pkg/clients/networking.go b/pkg/clients/networking.go
index c9e111b3ae..6cc4867d0c 100644
--- a/pkg/clients/networking.go
+++ b/pkg/clients/networking.go
@@ -26,6 +26,7 @@ import (
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers"
+ "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/qos/policies"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks"
@@ -48,6 +49,7 @@ type NetworkClient interface {
CreatePort(opts ports.CreateOptsBuilder) (*ports.Port, error)
DeletePort(id string) error
GetPort(id string) (*ports.Port, error)
+ GetPortWithQoS(id string) (*PortWithQoS, error)
UpdatePort(id string, opts ports.UpdateOptsBuilder) (*ports.Port, error)
ListTrunk(opts trunks.ListOptsBuilder) ([]trunks.Trunk, error)
@@ -88,11 +90,18 @@ type NetworkClient interface {
GetSubnet(id string) (*subnets.Subnet, error)
UpdateSubnet(id string, opts subnets.UpdateOptsBuilder) (*subnets.Subnet, error)
+ ListQoSPolicy(opts policies.ListOpts) ([]policies.Policy, error)
+
ListExtensions() ([]extensions.Extension, error)
ReplaceAllAttributesTags(resourceType string, resourceID string, opts attributestags.ReplaceAllOptsBuilder) ([]string, error)
}
+type PortWithQoS struct {
+ ports.Port
+ policies.QoSPolicyExt
+}
+
type networkClient struct {
serviceClient *gophercloud.ServiceClient
}
@@ -219,6 +228,16 @@ func (c networkClient) GetPort(id string) (*ports.Port, error) {
return port, nil
}
+func (c networkClient) GetPortWithQoS(id string) (*PortWithQoS, error) {
+ mc := metrics.NewMetricPrometheusContext("port", "get")
+ var port PortWithQoS
+ err := ports.Get(context.TODO(), c.serviceClient, id).ExtractInto(port)
+ if mc.ObserveRequestIgnoreNotFound(err) != nil {
+ return nil, err
+ }
+ return &port, nil
+}
+
func (c networkClient) UpdatePort(id string, opts ports.UpdateOptsBuilder) (*ports.Port, error) {
mc := metrics.NewMetricPrometheusContext("port", "update")
port, err := ports.Update(context.TODO(), c.serviceClient, id, opts).Extract()
@@ -456,6 +475,15 @@ func (c networkClient) UpdateSubnet(id string, opts subnets.UpdateOptsBuilder) (
return subnet, nil
}
+func (c networkClient) ListQoSPolicy(opts policies.ListOpts) ([]policies.Policy, error) {
+ mc := metrics.NewMetricPrometheusContext("qos_policy", "list")
+ allPages, err := policies.List(c.serviceClient, opts).AllPages(context.TODO())
+ if mc.ObserveRequest(err) != nil {
+ return nil, err
+ }
+ return policies.ExtractPolicies(allPages)
+}
+
func (c networkClient) ListExtensions() ([]extensions.Extension, error) {
mc := metrics.NewMetricPrometheusContext("network_extension", "list")
allPages, err := extensions.List(c.serviceClient).AllPages(context.TODO())
diff --git a/pkg/cloud/services/networking/port.go b/pkg/cloud/services/networking/port.go
index d284375e21..c06046b204 100644
--- a/pkg/cloud/services/networking/port.go
+++ b/pkg/cloud/services/networking/port.go
@@ -27,6 +27,7 @@ import (
"github.com/gophercloud/gophercloud/v2"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/portsbinding"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/portsecurity"
+ "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/qos/policies"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
@@ -37,6 +38,7 @@ import (
"sigs.k8s.io/cluster-api-provider-openstack/pkg/record"
"sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
capoerrors "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors"
+ "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/extensions"
"sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/names"
)
@@ -124,8 +126,8 @@ func (s *Service) GetPortForExternalNetwork(instanceID string, externalNetworkID
return nil, nil
}
-// ensurePortTagsAndTrunk ensures that the provided port has the tags and trunk defined in portSpec.
-func (s *Service) ensurePortTagsAndTrunk(port *ports.Port, eventObject runtime.Object, portSpec *infrav1.ResolvedPortSpec) error {
+// ensurePortProperties ensures that the provided port has the tags and trunk defined in portSpec.
+func (s *Service) ensurePortProperties(port *ports.Port, eventObject runtime.Object, portSpec *infrav1.ResolvedPortSpec) error {
wantedTags := uniqueSortedTags(portSpec.Tags)
actualTags := uniqueSortedTags(port.Tags)
// Only replace tags if there is a difference
@@ -149,6 +151,27 @@ func (s *Service) ensurePortTagsAndTrunk(port *ports.Port, eventObject runtime.O
}
}
}
+ if portSpec.QoSPolicyID != nil {
+ qosSupported, err := s.IsExtensionSupported(extensions.QoSExtensionName)
+ if err != nil {
+ return err
+ }
+
+ if !qosSupported {
+ return fmt.Errorf("there is no qos support. please ensure that the qos neutron extension is enabled in your OpenStack deployment")
+ }
+ portWithQoS, err := s.client.GetPortWithQoS(port.ID)
+ if err != nil {
+ return err
+ }
+ if *portSpec.QoSPolicyID != portWithQoS.QoSPolicyID {
+ if err = s.updatePortQoSPolicy(portWithQoS.ID, *portSpec.QoSPolicyID); err != nil {
+ record.Warnf(eventObject, "FailedReplaceQoSPolicy", "Failed to replace qos policy for port%s: %v", portWithQoS.Name, err)
+ return err
+ }
+ record.Eventf(eventObject, "SuccessfulReplaceQoSPolicy", "Replaced qos policy for port %s with %s", portWithQoS.Name, portSpec.QoSPolicyID)
+ }
+ }
return nil
}
@@ -174,7 +197,7 @@ func (s *Service) EnsurePort(eventObject runtime.Object, portSpec *infrav1.Resol
if len(existingPorts) == 1 {
port := &existingPorts[0]
- if err = s.ensurePortTagsAndTrunk(port, eventObject, portSpec); err != nil {
+ if err = s.ensurePortProperties(port, eventObject, portSpec); err != nil {
return nil, err
}
return port, nil
@@ -240,6 +263,15 @@ func (s *Service) EnsurePort(eventObject runtime.Object, portSpec *infrav1.Resol
builder = portSecurityOpts
}
+ if portSpec.QoSPolicyID != nil {
+ qosPolicyID := *portSpec.QoSPolicyID
+ portQosOpts := policies.PortCreateOptsExt{
+ CreateOptsBuilder: builder,
+ QoSPolicyID: qosPolicyID,
+ }
+ builder = portQosOpts
+ }
+
portsBindingOpts := portsbinding.CreateOptsExt{
CreateOptsBuilder: builder,
HostID: ptr.Deref(portSpec.HostID, ""),
@@ -254,7 +286,7 @@ func (s *Service) EnsurePort(eventObject runtime.Object, portSpec *infrav1.Resol
return nil, err
}
- if err = s.ensurePortTagsAndTrunk(port, eventObject, portSpec); err != nil {
+ if err = s.ensurePortProperties(port, eventObject, portSpec); err != nil {
return nil, err
}
record.Eventf(eventObject, "SuccessfulCreatePort", "Created port %s with id %s", port.Name, port.ID)
@@ -433,7 +465,7 @@ func (s *Service) ConstructPorts(instancePorts []infrav1.PortOpts, instanceSecur
return false
}
if portUsesTrunk() {
- trunkSupported, err := s.IsTrunkExtSupported()
+ trunkSupported, err := s.IsExtensionSupported(extensions.TrunkExtensionName)
if err != nil {
return nil, err
}
@@ -443,6 +475,26 @@ func (s *Service) ConstructPorts(instancePorts []infrav1.PortOpts, instanceSecur
}
}
+ // qos support is required if any port has QoSPolicy set
+ portUsesQoSPolicy := func() bool {
+ for _, port := range resolvedPorts {
+ if port.QoSPolicyID != nil {
+ return true
+ }
+ }
+ return false
+ }
+ if portUsesQoSPolicy() {
+ qosSupported, err := s.IsExtensionSupported(extensions.QoSExtensionName)
+ if err != nil {
+ return nil, err
+ }
+
+ if !qosSupported {
+ return nil, fmt.Errorf("there is no qos support. please ensure that the qos neutron extension is enabled in your OpenStack deployment")
+ }
+ }
+
return resolvedPorts, nil
}
@@ -487,6 +539,15 @@ func (s *Service) normalizePorts(ports []infrav1.PortOpts, clusterResourceName,
return nil, err
}
+ // Resolve QoS Policy ID
+ if port.QoSPolicy != nil {
+ policyID, err := s.GetQoSPolicyIDByParam(port.QoSPolicy)
+ if err != nil {
+ return nil, err
+ }
+ normalizedPort.QoSPolicyID = &policyID
+ }
+
// Resolve security groups when port security is not disabled
if !ptr.Deref(port.DisablePortSecurity, false) {
if len(port.SecurityGroups) == 0 {
@@ -595,18 +656,6 @@ func (s *Service) normalizePortTarget(port *infrav1.PortOpts, defaultNetwork *in
return networkID, resolvedFixedIPs, nil
}
-// IsTrunkExtSupported verifies trunk setup on the OpenStack deployment.
-func (s *Service) IsTrunkExtSupported() (trunknSupported bool, err error) {
- trunkSupport, err := s.GetTrunkSupport()
- if err != nil {
- return false, fmt.Errorf("there was an issue verifying whether trunk support is available, Please try again later: %v", err)
- }
- if !trunkSupport {
- return false, nil
- }
- return true, nil
-}
-
// AdoptPortsServer looks for ports in desiredPorts which were previously created, and adds them to resources.Ports.
// A port matches if it has the same name and network ID as the desired port.
// TODO(emilien): remove this function: https://github.com/kubernetes-sigs/cluster-api-provider-openstack/pull/2071
@@ -655,6 +704,16 @@ func (s *Service) AdoptPortsServer(scope *scope.WithLogger, desiredPorts []infra
return nil
}
+// updatePortQoSPolicy updates the QoSPolicy of a port.
+func (s *Service) updatePortQoSPolicy(portID string, qosPolicyID string) error {
+ updateOpts := policies.PortUpdateOptsExt{
+ UpdateOptsBuilder: ports.UpdateOpts{},
+ QoSPolicyID: &qosPolicyID,
+ }
+ _, err := s.client.UpdatePort(portID, updateOpts)
+ return err
+}
+
// uniqueSortedTags returns a new, sorted slice where any duplicates have been removed.
func uniqueSortedTags(tags []string) []string {
// remove duplicate values from tags
diff --git a/pkg/cloud/services/networking/port_test.go b/pkg/cloud/services/networking/port_test.go
index a215e2efe8..0790f9d132 100644
--- a/pkg/cloud/services/networking/port_test.go
+++ b/pkg/cloud/services/networking/port_test.go
@@ -25,6 +25,7 @@ import (
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/portsbinding"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/portsecurity"
+ "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/qos/policies"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks"
@@ -37,8 +38,10 @@ import (
infrav1alpha1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha1"
infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1"
+ "sigs.k8s.io/cluster-api-provider-openstack/pkg/clients"
"sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock"
"sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
+ capoextensions "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/extensions"
)
func Test_EnsurePort(t *testing.T) {
@@ -51,6 +54,7 @@ func Test_EnsurePort(t *testing.T) {
hostID = "825c1b11-3dca-4bfe-a2d8-a3cc1964c8d5"
trunkID = "eb7541fa-5e2a-4cca-b2c3-dfa409b917ce"
portSecurityGroupID = "f51d1206-fc5a-4f7a-a5c0-2e03e44e4dc0"
+ qosPolicyID = "1f14c71c-56fb-4a42-bea3-ea23e66a9d8a"
ipAddress1 = "192.0.2.1"
ipAddress2 = "198.51.100.1"
macAddress = "de:ad:be:ef:fe:ed"
@@ -439,6 +443,93 @@ func Test_EnsurePort(t *testing.T) {
NetworkID: netID,
},
},
+ {
+ name: "creates port with QoSPolicyID correctly",
+ port: infrav1.ResolvedPortSpec{
+ Name: "test-port",
+ NetworkID: netID,
+ QoSPolicyID: ptr.To(qosPolicyID),
+ },
+ expect: func(m *mock.MockNetworkClientMockRecorder, g Gomega) {
+ var expectedCreateOpts ports.CreateOptsBuilder
+ expectedCreateOpts = ports.CreateOpts{
+ NetworkID: netID,
+ Name: "test-port",
+ }
+ expectedCreateOpts = policies.PortCreateOptsExt{
+ CreateOptsBuilder: expectedCreateOpts,
+ QoSPolicyID: qosPolicyID,
+ }
+ expectedCreateOpts = portsbinding.CreateOptsExt{
+ CreateOptsBuilder: expectedCreateOpts,
+ }
+ m.ListPort(ports.ListOpts{
+ Name: "test-port",
+ NetworkID: netID,
+ }).Return(nil, nil)
+
+ qosExtension := extensions.Extension{}
+ qosExtension.Alias = capoextensions.QoSExtensionName
+ m.ListExtensions().Return([]extensions.Extension{qosExtension}, nil)
+ m.CreatePort(gomock.Any()).DoAndReturn(func(builder ports.CreateOptsBuilder) (*ports.Port, error) {
+ gotCreateOpts := builder.(portsbinding.CreateOptsExt)
+ g.Expect(gotCreateOpts).To(Equal(expectedCreateOpts), cmp.Diff(gotCreateOpts, expectedCreateOpts))
+ return &ports.Port{ID: portID}, nil
+ })
+ portWithQos := clients.PortWithQoS{
+ Port: ports.Port{
+ ID: portID,
+ },
+ QoSPolicyExt: policies.QoSPolicyExt{
+ QoSPolicyID: qosPolicyID,
+ },
+ }
+ m.GetPortWithQoS(portID).Return(&portWithQos, nil)
+ },
+ want: &ports.Port{ID: portID},
+ },
+ {
+ name: "Updates port with new QoSPolicyID",
+ port: infrav1.ResolvedPortSpec{
+ Name: "test-port",
+ NetworkID: netID,
+ QoSPolicyID: ptr.To(qosPolicyID),
+ },
+ expect: func(m *mock.MockNetworkClientMockRecorder, g Gomega) {
+ expectedPort := ports.Port{
+ ID: portID,
+ Name: "test-port",
+ NetworkID: netID,
+ }
+
+ m.ListPort(ports.ListOpts{
+ Name: "test-port",
+ NetworkID: netID,
+ }).Return([]ports.Port{expectedPort}, nil)
+
+ qosExtension := extensions.Extension{}
+ qosExtension.Alias = capoextensions.QoSExtensionName
+ m.ListExtensions().Return([]extensions.Extension{qosExtension}, nil)
+
+ portWithQos := clients.PortWithQoS{
+ Port: expectedPort,
+ QoSPolicyExt: policies.QoSPolicyExt{
+ QoSPolicyID: netID,
+ },
+ }
+ m.GetPortWithQoS(portID).Return(&portWithQos, nil)
+ m.UpdatePort(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(id string, builder ports.UpdateOptsBuilder) (*ports.Port, error) {
+ g.Expect(id).To(Equal(portID))
+ got := builder.(policies.PortUpdateOptsExt)
+ g.Expect(got.UpdateOptsBuilder).To(Equal(ports.UpdateOpts{}))
+ g.Expect(got.QoSPolicyID).ToNot(BeNil())
+ g.Expect(*got.QoSPolicyID).To(Equal(qosPolicyID))
+ return &expectedPort, nil
+ })
+ },
+ want: &ports.Port{ID: portID, Name: "test-port", NetworkID: netID},
+ },
}
eventObject := &infrav1.OpenStackMachine{}
@@ -485,7 +576,7 @@ func TestService_ConstructPorts(t *testing.T) {
expectListExtensions := func(m *mock.MockNetworkClientMockRecorder) {
trunkExtension := extensions.Extension{}
- trunkExtension.Alias = "trunk"
+ trunkExtension.Alias = capoextensions.TrunkExtensionName
m.ListExtensions().Return([]extensions.Extension{trunkExtension}, nil)
}
diff --git a/pkg/cloud/services/networking/qospolicy.go b/pkg/cloud/services/networking/qospolicy.go
new file mode 100644
index 0000000000..62cca7a0d2
--- /dev/null
+++ b/pkg/cloud/services/networking/qospolicy.go
@@ -0,0 +1,62 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package networking
+
+import (
+ "errors"
+
+ "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/qos/policies"
+
+ infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1"
+ capoerrors "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors"
+ "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/filterconvert"
+)
+
+// GetQoSPolicyIDByParam returns a qos policy ID based on the params passed by
+// the user.
+func (s *Service) GetQoSPolicyIDByParam(policyParam *infrav1.QoSPolicyParam) (string, error) {
+ if policyParam.ID != nil {
+ return *policyParam.ID, nil
+ }
+
+ if policyParam.Filter == nil {
+ // Should have been caught by validation
+ return "", errors.New("invalid qos policy param, either ID or Filter must be set")
+ }
+
+ listOpts := filterconvert.QoSPolicyFilterToListOpts(policyParam.Filter)
+ policy, err := s.getQoSPolicyByFilter(listOpts)
+ if err != nil {
+ return "", err
+ }
+ return policy.ID, nil
+}
+
+func (s *Service) getQoSPolicyByFilter(opts policies.ListOpts) (*policies.Policy, error) {
+ policyList, err := s.client.ListQoSPolicy(opts)
+ if err != nil {
+ return nil, err
+ }
+
+ switch len(policyList) {
+ case 0:
+ return nil, capoerrors.ErrNoMatches
+ case 1:
+ return &policyList[0], nil
+ }
+ return nil, capoerrors.ErrMultipleMatches
+}
diff --git a/pkg/cloud/services/networking/qospolicy_test.go b/pkg/cloud/services/networking/qospolicy_test.go
new file mode 100644
index 0000000000..f848f33155
--- /dev/null
+++ b/pkg/cloud/services/networking/qospolicy_test.go
@@ -0,0 +1,121 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package networking
+
+import (
+ "testing"
+
+ "github.com/go-logr/logr/testr"
+ "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/qos/policies"
+ . "github.com/onsi/gomega"
+ "go.uber.org/mock/gomock"
+
+ infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1"
+ "sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock"
+ "sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
+ capoerrors "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors"
+ "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/filterconvert"
+)
+
+func Test_GetQoSPolicyIDByParam(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ g := NewWithT(t)
+ mockClient := mock.NewMockNetworkClient(mockCtrl)
+
+ idDirect := "7fd24ceb-788a-441f-ad0a-d8e2f5d31a1d"
+ idGold := "d9c88a6d-0b8c-48ff-8f0e-8d85a078c194"
+
+ tests := []struct {
+ name string
+ param *infrav1.QoSPolicyParam
+ expect func()
+ wantID string
+ wantErr string
+ }{
+ {
+ name: "ID short-circuit (no client call)",
+ param: &infrav1.QoSPolicyParam{ID: &idDirect},
+ expect: func() {},
+ wantID: idDirect,
+ },
+ {
+ name: "nil ID & nil Filter -> validation error",
+ param: &infrav1.QoSPolicyParam{},
+ expect: func() {},
+ wantErr: "invalid qos policy param",
+ },
+ {
+ name: "filter -> single match returns ID",
+ param: &infrav1.QoSPolicyParam{
+ Filter: &infrav1.QoSPolicyFilter{Name: "gold"},
+ },
+ expect: func() {
+ opts := filterconvert.QoSPolicyFilterToListOpts(&infrav1.QoSPolicyFilter{Name: "gold"})
+ mockClient.EXPECT().ListQoSPolicy(opts).
+ Return([]policies.Policy{{ID: idGold, Name: "gold"}}, nil)
+ },
+ wantID: idGold,
+ },
+ {
+ name: "filter -> no matches propagates ErrNoMatches",
+ param: &infrav1.QoSPolicyParam{
+ Filter: &infrav1.QoSPolicyFilter{Name: "none"},
+ },
+ expect: func() {
+ opts := filterconvert.QoSPolicyFilterToListOpts(&infrav1.QoSPolicyFilter{Name: "none"})
+ mockClient.EXPECT().ListQoSPolicy(opts).
+ Return([]policies.Policy{}, nil)
+ },
+ wantErr: capoerrors.ErrNoMatches.Error(),
+ },
+ {
+ name: "filter -> multiple matches propagates ErrMultipleMatches",
+ param: &infrav1.QoSPolicyParam{
+ Filter: &infrav1.QoSPolicyFilter{Description: "dup"},
+ },
+ expect: func() {
+ opts := filterconvert.QoSPolicyFilterToListOpts(&infrav1.QoSPolicyFilter{Description: "dup"})
+ mockClient.EXPECT().ListQoSPolicy(opts).
+ Return([]policies.Policy{{ID: "a"}, {ID: "b"}}, nil)
+ },
+ wantErr: capoerrors.ErrMultipleMatches.Error(),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tt.expect()
+
+ scopeFactory := scope.NewMockScopeFactory(mockCtrl, "")
+ log := testr.New(t)
+ s := Service{
+ client: mockClient,
+ scope: scope.NewWithLogger(scopeFactory, log),
+ }
+ got, err := s.GetQoSPolicyIDByParam(tt.param)
+ if tt.wantErr != "" {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
+ return
+ }
+ g.Expect(err).NotTo(HaveOccurred())
+ g.Expect(got).To(Equal(tt.wantID))
+ })
+ }
+}
diff --git a/pkg/cloud/services/networking/service.go b/pkg/cloud/services/networking/service.go
index e8a5b117ad..7a0873620b 100644
--- a/pkg/cloud/services/networking/service.go
+++ b/pkg/cloud/services/networking/service.go
@@ -76,3 +76,19 @@ func (s *Service) replaceAllAttributesTags(eventObject runtime.Object, resourceT
record.Eventf(eventObject, "SuccessfulReplaceAllAttributeTags", "Replaced all attributestags for %s with tags %s", resourceID, tags)
return nil
}
+
+// IsExtensionSupported checks whether a given extension is
+// supported on the openstack deployment.
+func (s *Service) IsExtensionSupported(extensionName string) (bool, error) {
+ allExts, err := s.client.ListExtensions()
+ if err != nil {
+ return false, fmt.Errorf("there was an issue verifying whether %s support is available, Please try again later: %v", extensionName, err)
+ }
+
+ for _, ext := range allExts {
+ if ext.Alias == extensionName {
+ return true, nil
+ }
+ }
+ return false, nil
+}
diff --git a/pkg/cloud/services/networking/trunk.go b/pkg/cloud/services/networking/trunk.go
index ffe393de78..3f025cdad1 100644
--- a/pkg/cloud/services/networking/trunk.go
+++ b/pkg/cloud/services/networking/trunk.go
@@ -36,20 +36,6 @@ const (
retryIntervalSubportDelete = 30 * time.Second
)
-func (s *Service) GetTrunkSupport() (bool, error) {
- allExts, err := s.client.ListExtensions()
- if err != nil {
- return false, err
- }
-
- for _, ext := range allExts {
- if ext.Alias == "trunk" {
- return true, nil
- }
- }
- return false, nil
-}
-
func (s *Service) getOrCreateTrunkForPort(eventObject runtime.Object, port *ports.Port) (*trunks.Trunk, error) {
trunkList, err := s.client.ListTrunk(trunks.ListOpts{
Name: port.Name,
diff --git a/pkg/generated/applyconfiguration/api/v1beta1/portopts.go b/pkg/generated/applyconfiguration/api/v1beta1/portopts.go
index a3f365daf3..9e68ef4538 100644
--- a/pkg/generated/applyconfiguration/api/v1beta1/portopts.go
+++ b/pkg/generated/applyconfiguration/api/v1beta1/portopts.go
@@ -28,6 +28,7 @@ type PortOptsApplyConfiguration struct {
SecurityGroups []SecurityGroupParamApplyConfiguration `json:"securityGroups,omitempty"`
Tags []string `json:"tags,omitempty"`
Trunk *bool `json:"trunk,omitempty"`
+ QoSPolicy *QoSPolicyParamApplyConfiguration `json:"qosPolicy,omitempty"`
ResolvedPortSpecFieldsApplyConfiguration `json:",inline"`
}
@@ -105,6 +106,14 @@ func (b *PortOptsApplyConfiguration) WithTrunk(value bool) *PortOptsApplyConfigu
return b
}
+// WithQoSPolicy sets the QoSPolicy field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the QoSPolicy field is set to the value of the last call.
+func (b *PortOptsApplyConfiguration) WithQoSPolicy(value *QoSPolicyParamApplyConfiguration) *PortOptsApplyConfiguration {
+ b.QoSPolicy = value
+ return b
+}
+
// WithAdminStateUp sets the AdminStateUp field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the AdminStateUp field is set to the value of the last call.
diff --git a/pkg/generated/applyconfiguration/api/v1beta1/qospolicyfilter.go b/pkg/generated/applyconfiguration/api/v1beta1/qospolicyfilter.go
new file mode 100644
index 0000000000..6aff299fac
--- /dev/null
+++ b/pkg/generated/applyconfiguration/api/v1beta1/qospolicyfilter.go
@@ -0,0 +1,120 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+ apiv1beta1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1"
+)
+
+// QoSPolicyFilterApplyConfiguration represents a declarative configuration of the QoSPolicyFilter type for use
+// with apply.
+type QoSPolicyFilterApplyConfiguration struct {
+ Name *string `json:"name,omitempty"`
+ Description *string `json:"description,omitempty"`
+ ProjectID *string `json:"projectID,omitempty"`
+ Shared *bool `json:"shared,omitempty"`
+ IsDefault *bool `json:"isDefault,omitempty"`
+ FilterByNeutronTagsApplyConfiguration `json:",inline"`
+}
+
+// QoSPolicyFilterApplyConfiguration constructs a declarative configuration of the QoSPolicyFilter type for use with
+// apply.
+func QoSPolicyFilter() *QoSPolicyFilterApplyConfiguration {
+ return &QoSPolicyFilterApplyConfiguration{}
+}
+
+// WithName sets the Name field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Name field is set to the value of the last call.
+func (b *QoSPolicyFilterApplyConfiguration) WithName(value string) *QoSPolicyFilterApplyConfiguration {
+ b.Name = &value
+ return b
+}
+
+// WithDescription sets the Description field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Description field is set to the value of the last call.
+func (b *QoSPolicyFilterApplyConfiguration) WithDescription(value string) *QoSPolicyFilterApplyConfiguration {
+ b.Description = &value
+ return b
+}
+
+// WithProjectID sets the ProjectID field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ProjectID field is set to the value of the last call.
+func (b *QoSPolicyFilterApplyConfiguration) WithProjectID(value string) *QoSPolicyFilterApplyConfiguration {
+ b.ProjectID = &value
+ return b
+}
+
+// WithShared sets the Shared field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Shared field is set to the value of the last call.
+func (b *QoSPolicyFilterApplyConfiguration) WithShared(value bool) *QoSPolicyFilterApplyConfiguration {
+ b.Shared = &value
+ return b
+}
+
+// WithIsDefault sets the IsDefault field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the IsDefault field is set to the value of the last call.
+func (b *QoSPolicyFilterApplyConfiguration) WithIsDefault(value bool) *QoSPolicyFilterApplyConfiguration {
+ b.IsDefault = &value
+ return b
+}
+
+// WithTags adds the given value to the Tags field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Tags field.
+func (b *QoSPolicyFilterApplyConfiguration) WithTags(values ...apiv1beta1.NeutronTag) *QoSPolicyFilterApplyConfiguration {
+ for i := range values {
+ b.FilterByNeutronTagsApplyConfiguration.Tags = append(b.FilterByNeutronTagsApplyConfiguration.Tags, values[i])
+ }
+ return b
+}
+
+// WithTagsAny adds the given value to the TagsAny field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the TagsAny field.
+func (b *QoSPolicyFilterApplyConfiguration) WithTagsAny(values ...apiv1beta1.NeutronTag) *QoSPolicyFilterApplyConfiguration {
+ for i := range values {
+ b.FilterByNeutronTagsApplyConfiguration.TagsAny = append(b.FilterByNeutronTagsApplyConfiguration.TagsAny, values[i])
+ }
+ return b
+}
+
+// WithNotTags adds the given value to the NotTags field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the NotTags field.
+func (b *QoSPolicyFilterApplyConfiguration) WithNotTags(values ...apiv1beta1.NeutronTag) *QoSPolicyFilterApplyConfiguration {
+ for i := range values {
+ b.FilterByNeutronTagsApplyConfiguration.NotTags = append(b.FilterByNeutronTagsApplyConfiguration.NotTags, values[i])
+ }
+ return b
+}
+
+// WithNotTagsAny adds the given value to the NotTagsAny field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the NotTagsAny field.
+func (b *QoSPolicyFilterApplyConfiguration) WithNotTagsAny(values ...apiv1beta1.NeutronTag) *QoSPolicyFilterApplyConfiguration {
+ for i := range values {
+ b.FilterByNeutronTagsApplyConfiguration.NotTagsAny = append(b.FilterByNeutronTagsApplyConfiguration.NotTagsAny, values[i])
+ }
+ return b
+}
diff --git a/pkg/generated/applyconfiguration/api/v1beta1/qospolicyparam.go b/pkg/generated/applyconfiguration/api/v1beta1/qospolicyparam.go
new file mode 100644
index 0000000000..dad91ab230
--- /dev/null
+++ b/pkg/generated/applyconfiguration/api/v1beta1/qospolicyparam.go
@@ -0,0 +1,48 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1beta1
+
+// QoSPolicyParamApplyConfiguration represents a declarative configuration of the QoSPolicyParam type for use
+// with apply.
+type QoSPolicyParamApplyConfiguration struct {
+ ID *string `json:"id,omitempty"`
+ Filter *QoSPolicyFilterApplyConfiguration `json:"filter,omitempty"`
+}
+
+// QoSPolicyParamApplyConfiguration constructs a declarative configuration of the QoSPolicyParam type for use with
+// apply.
+func QoSPolicyParam() *QoSPolicyParamApplyConfiguration {
+ return &QoSPolicyParamApplyConfiguration{}
+}
+
+// WithID sets the ID field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ID field is set to the value of the last call.
+func (b *QoSPolicyParamApplyConfiguration) WithID(value string) *QoSPolicyParamApplyConfiguration {
+ b.ID = &value
+ return b
+}
+
+// WithFilter sets the Filter field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Filter field is set to the value of the last call.
+func (b *QoSPolicyParamApplyConfiguration) WithFilter(value *QoSPolicyFilterApplyConfiguration) *QoSPolicyParamApplyConfiguration {
+ b.Filter = value
+ return b
+}
diff --git a/pkg/generated/applyconfiguration/api/v1beta1/resolvedportspec.go b/pkg/generated/applyconfiguration/api/v1beta1/resolvedportspec.go
index 975f056e91..f6134d6569 100644
--- a/pkg/generated/applyconfiguration/api/v1beta1/resolvedportspec.go
+++ b/pkg/generated/applyconfiguration/api/v1beta1/resolvedportspec.go
@@ -28,6 +28,7 @@ type ResolvedPortSpecApplyConfiguration struct {
Trunk *bool `json:"trunk,omitempty"`
FixedIPs []ResolvedFixedIPApplyConfiguration `json:"fixedIPs,omitempty"`
SecurityGroups []string `json:"securityGroups,omitempty"`
+ QoSPolicyID *string `json:"qosPolicyID,omitempty"`
ResolvedPortSpecFieldsApplyConfiguration `json:",inline"`
}
@@ -102,6 +103,14 @@ func (b *ResolvedPortSpecApplyConfiguration) WithSecurityGroups(values ...string
return b
}
+// WithQoSPolicyID sets the QoSPolicyID field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the QoSPolicyID field is set to the value of the last call.
+func (b *ResolvedPortSpecApplyConfiguration) WithQoSPolicyID(value string) *ResolvedPortSpecApplyConfiguration {
+ b.QoSPolicyID = &value
+ return b
+}
+
// WithAdminStateUp sets the AdminStateUp field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the AdminStateUp field is set to the value of the last call.
diff --git a/pkg/generated/applyconfiguration/internal/internal.go b/pkg/generated/applyconfiguration/internal/internal.go
index b11934fa54..69bf01d082 100644
--- a/pkg/generated/applyconfiguration/internal/internal.go
+++ b/pkg/generated/applyconfiguration/internal/internal.go
@@ -1203,6 +1203,9 @@ var schemaYAML = typed.YAMLObject(`types:
- name: propagateUplinkStatus
type:
scalar: boolean
+ - name: qosPolicy
+ type:
+ namedType: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.QoSPolicyParam
- name: securityGroups
type:
list:
@@ -1236,6 +1239,57 @@ var schemaYAML = typed.YAMLObject(`types:
type:
scalar: string
default: ""
+- name: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.QoSPolicyFilter
+ map:
+ fields:
+ - name: description
+ type:
+ scalar: string
+ - name: isDefault
+ type:
+ scalar: boolean
+ - name: name
+ type:
+ scalar: string
+ - name: notTags
+ type:
+ list:
+ elementType:
+ scalar: string
+ elementRelationship: associative
+ - name: notTagsAny
+ type:
+ list:
+ elementType:
+ scalar: string
+ elementRelationship: associative
+ - name: projectID
+ type:
+ scalar: string
+ - name: shared
+ type:
+ scalar: boolean
+ - name: tags
+ type:
+ list:
+ elementType:
+ scalar: string
+ elementRelationship: associative
+ - name: tagsAny
+ type:
+ list:
+ elementType:
+ scalar: string
+ elementRelationship: associative
+- name: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.QoSPolicyParam
+ map:
+ fields:
+ - name: filter
+ type:
+ namedType: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.QoSPolicyFilter
+ - name: id
+ type:
+ scalar: string
- name: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.ResolvedFixedIP
map:
fields:
@@ -1308,6 +1362,9 @@ var schemaYAML = typed.YAMLObject(`types:
- name: propagateUplinkStatus
type:
scalar: boolean
+ - name: qosPolicyID
+ type:
+ scalar: string
- name: securityGroups
type:
list:
diff --git a/pkg/generated/applyconfiguration/utils.go b/pkg/generated/applyconfiguration/utils.go
index 494dc2afa8..d6e79d1487 100644
--- a/pkg/generated/applyconfiguration/utils.go
+++ b/pkg/generated/applyconfiguration/utils.go
@@ -126,6 +126,10 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
return &apiv1beta1.PortOptsApplyConfiguration{}
case v1beta1.SchemeGroupVersion.WithKind("PortStatus"):
return &apiv1beta1.PortStatusApplyConfiguration{}
+ case v1beta1.SchemeGroupVersion.WithKind("QoSPolicyFilter"):
+ return &apiv1beta1.QoSPolicyFilterApplyConfiguration{}
+ case v1beta1.SchemeGroupVersion.WithKind("QoSPolicyParam"):
+ return &apiv1beta1.QoSPolicyParamApplyConfiguration{}
case v1beta1.SchemeGroupVersion.WithKind("ResolvedFixedIP"):
return &apiv1beta1.ResolvedFixedIPApplyConfiguration{}
case v1beta1.SchemeGroupVersion.WithKind("ResolvedMachineSpec"):
diff --git a/pkg/utils/extensions/extensions.go b/pkg/utils/extensions/extensions.go
new file mode 100644
index 0000000000..49797c622e
--- /dev/null
+++ b/pkg/utils/extensions/extensions.go
@@ -0,0 +1,22 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package extensions
+
+const (
+ TrunkExtensionName = "trunk"
+ QoSExtensionName = "qos"
+)
diff --git a/pkg/utils/filterconvert/convert.go b/pkg/utils/filterconvert/convert.go
index 25346b7d3f..59e2261f29 100644
--- a/pkg/utils/filterconvert/convert.go
+++ b/pkg/utils/filterconvert/convert.go
@@ -19,6 +19,7 @@ package filterconvert
import (
"github.com/gophercloud/gophercloud/v2/openstack/image/v2/images"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers"
+ "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/qos/policies"
securitygroups "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets"
@@ -91,6 +92,23 @@ func RouterFilterToListOpts(routerFilter *infrav1.RouterFilter) routers.ListOpts
}
}
+func QoSPolicyFilterToListOpts(policyFilter *infrav1.QoSPolicyFilter) policies.ListOpts {
+ if policyFilter == nil {
+ return policies.ListOpts{}
+ }
+ return policies.ListOpts{
+ Name: policyFilter.Name,
+ Description: policyFilter.Description,
+ IsDefault: policyFilter.IsDefault,
+ Shared: policyFilter.Shared,
+ ProjectID: policyFilter.ProjectID,
+ Tags: infrav1.JoinTags(policyFilter.Tags),
+ TagsAny: infrav1.JoinTags(policyFilter.TagsAny),
+ NotTags: infrav1.JoinTags(policyFilter.NotTags),
+ NotTagsAny: infrav1.JoinTags(policyFilter.NotTagsAny),
+ }
+}
+
func ImageFilterToListOpts(imageFilter *infrav1.ImageFilter) (listOpts images.ListOpts) {
if imageFilter == nil {
return listOpts
diff --git a/test/e2e/data/kustomize/multi-network/patch-machine-template-networks.yaml b/test/e2e/data/kustomize/multi-network/patch-machine-template-networks.yaml
index 4ad2116cfb..525dc2c1bb 100644
--- a/test/e2e/data/kustomize/multi-network/patch-machine-template-networks.yaml
+++ b/test/e2e/data/kustomize/multi-network/patch-machine-template-networks.yaml
@@ -3,9 +3,17 @@
path: /spec/template/spec/ports
value:
- description: "primary"
+ qosPolicy:
+ id: "${QOS_POLICY_ID}"
- description: "Extra Network 1"
network:
id: "${CLUSTER_EXTRA_NET_1}"
+ qosPolicy:
+ filter:
+ name: "${QOS_POLICY_NAME}"
- description: "Extra Network 2"
network:
id: "${CLUSTER_EXTRA_NET_2}"
+ qosPolicy:
+ filter:
+ shared: true
diff --git a/test/e2e/shared/openstack.go b/test/e2e/shared/openstack.go
index 7c7128b763..63d5feb747 100644
--- a/test/e2e/shared/openstack.go
+++ b/test/e2e/shared/openstack.go
@@ -42,6 +42,7 @@ import (
"github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers"
"github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers"
+ "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/qos/policies"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks"
@@ -67,6 +68,11 @@ type ServerExtWithIP struct {
ip string
}
+type PortWithQoS struct {
+ ports.Port
+ policies.QoSPolicyExt
+}
+
// ensureSSHKeyPair ensures A SSH key is present under the name.
func ensureSSHKeyPair(e2eCtx *E2EContext) {
Logf("Ensuring presence of SSH key %q in OpenStack", DefaultSSHKeyPairName)
@@ -1027,3 +1033,76 @@ func GetOpenStackServerConsoleLog(e2eCtx *E2EContext, id string) (string, error)
}
return computeClient.GetConsoleOutput(id)
}
+
+// CreateOpenStackQoSPolicy creates a qos policy to be consumed by a nodes.
+func CreateOpenStackQoSPolicy(ctx context.Context, e2eCtx *E2EContext, policyName string) (*policies.Policy, error) {
+ providerClient, clientOpts, _, err := GetAdminProviderClient(e2eCtx)
+ if err != nil {
+ return nil, fmt.Errorf("error creating provider client: %s", err)
+ }
+
+ networkClient, err := openstack.NewNetworkV2(providerClient, gophercloud.EndpointOpts{
+ Region: clientOpts.RegionName,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("error creating network client: %s", err)
+ }
+
+ createOpts := policies.CreateOpts{
+ Name: policyName,
+ Shared: true,
+ IsDefault: false,
+ }
+
+ policy, err := policies.Create(ctx, networkClient, createOpts).Extract()
+ if err != nil {
+ return nil, err
+ }
+
+ return policy, nil
+}
+
+// DeleteOpenStackQoSPolicy deletes a qos policy.
+func DeleteOpenStackQoSPolicy(ctx context.Context, e2eCtx *E2EContext, policyID string) error {
+ providerClient, clientOpts, _, err := GetAdminProviderClient(e2eCtx)
+ if err != nil {
+ return fmt.Errorf("error creating provider client: %s", err)
+ }
+
+ networkClient, err := openstack.NewNetworkV2(providerClient, gophercloud.EndpointOpts{
+ Region: clientOpts.RegionName,
+ })
+ if err != nil {
+ return fmt.Errorf("error creating network client: %s", err)
+ }
+
+ err = policies.Delete(ctx, networkClient, policyID).ExtractErr()
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// GetOpenStackPortWithQoSPolicy gets a neutron port with info about the qos policy.
+func GetOpenStackPortWithQoSPolicy(ctx context.Context, e2eCtx *E2EContext, portID string) (*PortWithQoS, error) {
+ providerClient, clientOpts, _, err := GetTenantProviderClient(e2eCtx)
+ if err != nil {
+ return nil, fmt.Errorf("error creating provider client: %s", err)
+ }
+
+ networkClient, err := openstack.NewNetworkV2(providerClient, gophercloud.EndpointOpts{
+ Region: clientOpts.RegionName,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("error creating network client: %s", err)
+ }
+
+ var port PortWithQoS
+ err = ports.Get(ctx, networkClient, portID).ExtractInto(&port)
+ if err != nil {
+ return nil, err
+ }
+
+ return &port, nil
+}
diff --git a/test/e2e/suites/e2e/e2e_test.go b/test/e2e/suites/e2e/e2e_test.go
index 3fbdd48627..9b7e0de33e 100644
--- a/test/e2e/suites/e2e/e2e_test.go
+++ b/test/e2e/suites/e2e/e2e_test.go
@@ -35,6 +35,7 @@ import (
"github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers"
"github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers"
+ "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/qos/policies"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks"
@@ -680,13 +681,14 @@ var _ = Describe("e2e tests [PR-Blocking]", func() {
})
})
- Describe("Workload cluster (multiple attached networks)", func() {
+ Describe("Workload cluster (multiple attached networks and qos policies)", func() {
var (
clusterName string
configCluster clusterctl.ConfigClusterInput
md []*clusterv1.MachineDeployment
extraNet1, extraNet2 *networks.Network
+ qosPolicy *policies.Policy
)
BeforeEach(func(ctx context.Context) {
@@ -717,6 +719,19 @@ var _ = Describe("e2e tests [PR-Blocking]", func() {
os.Setenv("CLUSTER_EXTRA_NET_1", extraNet1.ID)
os.Setenv("CLUSTER_EXTRA_NET_2", extraNet2.ID)
+ shared.Logf("Creating qos policy")
+
+ qosPolicy, err = shared.CreateOpenStackQoSPolicy(ctx, e2eCtx, fmt.Sprintf("%s-qos-policy", namespace.Name))
+ Expect(err).NotTo(HaveOccurred())
+ postClusterCleanup = append(postClusterCleanup, func(ctx context.Context) {
+ shared.Logf("Deleting qos policy %s", qosPolicy.ID)
+ err := shared.DeleteOpenStackQoSPolicy(ctx, e2eCtx, qosPolicy.ID)
+ Expect(err).NotTo(HaveOccurred())
+ })
+
+ os.Setenv("QOS_POLICY_ID", qosPolicy.ID)
+ os.Setenv("QOS_POLICY_NAME", qosPolicy.Name)
+
shared.Logf("Creating a cluster")
clusterName = fmt.Sprintf("cluster-%s", namespace.Name)
configCluster = defaultConfigCluster(clusterName, namespace.Name)
@@ -727,7 +742,7 @@ var _ = Describe("e2e tests [PR-Blocking]", func() {
md = clusterResources.MachineDeployments
})
- It("should attach all machines to multiple networks", func(ctx context.Context) {
+ It("should attach all machines to multiple networks and qos policy", func(ctx context.Context) {
workerMachines := framework.GetMachinesByMachineDeployments(ctx, framework.GetMachinesByMachineDeploymentsInput{
Lister: e2eCtx.Environment.BootstrapClusterProxy.GetClient(),
ClusterName: clusterName,
@@ -756,6 +771,8 @@ var _ = Describe("e2e tests [PR-Blocking]", func() {
extraNet2.ID: "Extra Network 2",
}
+ expectedPolicyID := qosPolicy.ID
+
for i := range allMachines {
machine := &allMachines[i]
shared.Logf("Checking ports for machine %s", machine.Name)
@@ -788,6 +805,14 @@ var _ = Describe("e2e tests [PR-Blocking]", func() {
}
}
+ // all ports should have the same qos policy id
+ for j := range ports {
+ port := &ports[j]
+ qosPort, err := shared.GetOpenStackPortWithQoSPolicy(ctx, e2eCtx, port.ID)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(qosPort.QoSPolicyID).To(Equal(expectedPolicyID))
+ }
+
// All IP addresses on all ports should be reported in Addresses
Expect(machine.Status.Addresses).To(ContainElements(seenAddresses))
|