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.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+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.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+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))