Skip to content

Commit 43b55ba

Browse files
authored
Implement automatic topology awareness for Manila share provisioning (#2255)
1 parent 37a490e commit 43b55ba

File tree

10 files changed

+142
-34
lines changed

10 files changed

+142
-34
lines changed

docs/manila-csi-plugin/using-manila-csi-plugin.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Parameter | Required | Description
5050
`type` | _yes_ | Manila [share type](https://wiki.openstack.org/wiki/Manila/Concepts#share_type)
5151
`shareNetworkID` | _no_ | Manila [share network ID](https://wiki.openstack.org/wiki/Manila/Concepts#share_network)
5252
`availability` | _no_ | Manila availability zone of the provisioned share. If none is provided, the default Manila zone will be used. Note that this parameter is opaque to the CO and does not influence placement of workloads that will consume this share, meaning they may be scheduled onto any node of the cluster. If the specified Manila AZ is not equally accessible from all compute nodes of the cluster, use [Topology-aware dynamic provisioning](#topology-aware-dynamic-provisioning).
53+
`autoTopology` | _no_ | When set to "true" and the `availability` parameter is empty, the Manila CSI controller will map the Manila availability zone to the target compute node availability zone.
5354
`appendShareMetadata` | _no_ | Append user-defined metadata to the provisioned share. If not empty, this field must be a string with a valid JSON object. The object must consist of key-value pairs of type string. Example: `"{..., \"key\": \"value\"}"`.
5455
`cephfs-mounter` | _no_ | Relevant for CephFS Manila shares. Specifies which mounting method to use with the CSI CephFS driver. Available options are `kernel` and `fuse`, defaults to `fuse`. See [CSI CephFS docs](https://github.com/ceph/ceph-csi/blob/csi-v1.0/docs/deploy-cephfs.md#configuration) for further information.
5556
`cephfs-kernelMountOptions` | _no_ | Relevant for CephFS Manila shares. Specifies mount options for CephFS kernel client. See [CSI CephFS docs](https://github.com/ceph/ceph-csi/blob/csi-v1.0/docs/deploy-cephfs.md#configuration) for further information.
@@ -130,6 +131,35 @@ Storage AZ does not influence
130131
Shares in zone-a are accessible only from nodes in nova-1 and nova-2.
131132
```
132133

134+
In cases when the Manila availability zone must correspond to the Nova
135+
availability zone, you can set the `autoTopology: "true"` along with the
136+
`volumeBindingMode: WaitForFirstConsumer` and omit the `availability`
137+
parameter. By doing so, the share will be provisioned in the target compute
138+
node availability zone.
139+
140+
```
141+
Auto topology-aware storage class example:
142+
143+
144+
Both Compute and Storage AZs influence the placement of workloads.
145+
146+
+-----------+ +---------------+
147+
| Manila AZ | | Compute AZs |
148+
| zone-1 | apiVersion: storage.k8s.io/v1 | zone-1 |
149+
| zone-2 | kind: StorageClass | zone-2 |
150+
+-----------+ metadata: +---------------+
151+
| name: nfs-gold |
152+
| provisioner: nfs.manila.csi.openstack.org |
153+
| parameters: |
154+
+---------+ autoTopology: "true" +--------------------+
155+
...
156+
volumeBindingMode: WaitForFirstConsumer
157+
...
158+
159+
Shares for workloads in zone-1 will be created in zone-1 and accessible only from nodes in zone-1.
160+
Shares for workloads in zone-2 will be created in zone-2 and accessible only from nodes in zone-2.
161+
```
162+
133163
[Enabling topology awareness in Kubernetes](#enabling-topology-awareness)
134164

135165
### Runtime configuration file
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: v1
2+
kind: Pod
3+
metadata:
4+
name: new-nfs-share-pod
5+
spec:
6+
containers:
7+
- name: web-server
8+
image: nginx
9+
imagePullPolicy: IfNotPresent
10+
volumeMounts:
11+
- name: mypvc
12+
mountPath: /var/lib/www
13+
nodeSelector:
14+
topology.kubernetes.io/zone: zone-1
15+
volumes:
16+
- name: mypvc
17+
persistentVolumeClaim:
18+
claimName: new-nfs-share-pvc
19+
readOnly: false
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: v1
2+
kind: PersistentVolumeClaim
3+
metadata:
4+
name: new-nfs-share-pvc
5+
spec:
6+
accessModes:
7+
- ReadWriteMany
8+
resources:
9+
requests:
10+
storage: 1Gi
11+
storageClassName: csi-manila-nfs
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Topology constraints example:
2+
#
3+
# Let's have two Manila AZs: zone-{1..2}
4+
# Let's have six Nova AZs: zone-{1..6}
5+
#
6+
# Manila zone-1 is accessible from nodes in zone-1 only
7+
# Manila zone-2 is accessible from nodes in zone-2 only
8+
#
9+
# We're provisioning into zone-1
10+
# availability parameter and allowedTopologies are empty, therefore the dynamic
11+
# share provisioning with automatic availability zone selection takes place.
12+
# The "volumeBindingMode" must be set to "WaitForFirstConsumer".
13+
14+
apiVersion: storage.k8s.io/v1
15+
kind: StorageClass
16+
metadata:
17+
name: csi-manila-nfs
18+
provisioner: nfs.manila.csi.openstack.org
19+
volumeBindingMode: WaitForFirstConsumer
20+
allowVolumeExpansion: true
21+
parameters:
22+
type: default
23+
autoTopology: "true"
24+
csi.storage.k8s.io/provisioner-secret-name: csi-manila-secrets
25+
csi.storage.k8s.io/provisioner-secret-namespace: default
26+
csi.storage.k8s.io/node-stage-secret-name: csi-manila-secrets
27+
csi.storage.k8s.io/node-stage-secret-namespace: default
28+
csi.storage.k8s.io/node-publish-secret-name: csi-manila-secrets
29+
csi.storage.k8s.io/node-publish-secret-namespace: default

examples/manila-csi-plugin/nfs/dynamic-provisioning/pvc.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,3 @@ spec:
99
requests:
1010
storage: 1Gi
1111
storageClassName: csi-manila-nfs
12-

examples/manila-csi-plugin/nfs/topology-aware/storageclass.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ parameters:
2424
csi.storage.k8s.io/node-stage-secret-namespace: default
2525
csi.storage.k8s.io/node-publish-secret-name: csi-manila-secrets
2626
csi.storage.k8s.io/node-publish-secret-namespace: default
27+
allowVolumeExpansion: true
2728
allowedTopologies:
2829
- matchLabelExpressions:
2930
- key: topology.manila.csi.openstack.org/zone

pkg/csi/cinder/controllerserver.go

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,13 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol
6969
// Volume Type
7070
volType := req.GetParameters()["type"]
7171

72-
var volAvailability string
73-
7472
// First check if volAvailability is already specified, if not get preferred from Topology
7573
// Required, incase vol AZ is different from node AZ
76-
volAvailability = req.GetParameters()["availability"]
77-
78-
if len(volAvailability) == 0 {
74+
volAvailability := req.GetParameters()["availability"]
75+
if volAvailability == "" {
7976
// Check from Topology
8077
if req.GetAccessibilityRequirements() != nil {
81-
volAvailability = getAZFromTopology(req.GetAccessibilityRequirements())
78+
volAvailability = util.GetAZFromTopology(topologyKey, req.GetAccessibilityRequirements())
8279
}
8380
}
8481

@@ -651,23 +648,6 @@ func (cs *controllerServer) ControllerExpandVolume(ctx context.Context, req *csi
651648
}, nil
652649
}
653650

654-
func getAZFromTopology(requirement *csi.TopologyRequirement) string {
655-
for _, topology := range requirement.GetPreferred() {
656-
zone, exists := topology.GetSegments()[topologyKey]
657-
if exists {
658-
return zone
659-
}
660-
}
661-
662-
for _, topology := range requirement.GetRequisite() {
663-
zone, exists := topology.GetSegments()[topologyKey]
664-
if exists {
665-
return zone
666-
}
667-
}
668-
return ""
669-
}
670-
671651
func getCreateVolumeResponse(vol *volumes.Volume, ignoreVolumeAZ bool, accessibleTopologyReq *csi.TopologyRequirement) *csi.CreateVolumeResponse {
672652

673653
var volsrc *csi.VolumeContentSource

pkg/csi/manila/controllerserver.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"k8s.io/cloud-provider-openstack/pkg/csi/manila/capabilities"
3131
"k8s.io/cloud-provider-openstack/pkg/csi/manila/options"
3232
"k8s.io/cloud-provider-openstack/pkg/csi/manila/shareadapters"
33+
"k8s.io/cloud-provider-openstack/pkg/util"
3334
clouderrors "k8s.io/cloud-provider-openstack/pkg/util/errors"
3435
"k8s.io/klog/v2"
3536
)
@@ -54,7 +55,7 @@ var (
5455
}
5556
)
5657

57-
func getVolumeCreator(source *csi.VolumeContentSource, shareOpts *options.ControllerVolumeContext, compatOpts *options.CompatibilityOptions) (volumeCreator, error) {
58+
func getVolumeCreator(source *csi.VolumeContentSource) (volumeCreator, error) {
5859
if source == nil {
5960
return &blankVolume{}, nil
6061
}
@@ -135,9 +136,25 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol
135136

136137
sizeInGiB := bytesToGiB(requestedSize)
137138

139+
var accessibleTopology []*csi.Topology
140+
accessibleTopologyReq := req.GetAccessibilityRequirements()
141+
if cs.d.withTopology && accessibleTopologyReq != nil {
142+
// All requisite/preferred topologies are considered valid. Nodes from those zones are required to be able to reach the storage.
143+
// The operator is responsible for making sure that provided topology keys are valid and present on the nodes of the cluster.
144+
accessibleTopology = accessibleTopologyReq.GetPreferred()
145+
146+
// When "autoTopology" is enabled and "availability" is empty, obtain the AZ from the target node.
147+
if shareOpts.AvailabilityZone == "" && strings.EqualFold(shareOpts.AutoTopology, "true") {
148+
shareOpts.AvailabilityZone = util.GetAZFromTopology(topologyKey, accessibleTopologyReq)
149+
accessibleTopology = []*csi.Topology{{
150+
Segments: map[string]string{topologyKey: shareOpts.AvailabilityZone},
151+
}}
152+
}
153+
}
154+
138155
// Retrieve an existing share or create a new one
139156

140-
volCreator, err := getVolumeCreator(req.GetVolumeContentSource(), shareOpts, cs.d.compatOpts)
157+
volCreator, err := getVolumeCreator(req.GetVolumeContentSource())
141158
if err != nil {
142159
return nil, err
143160
}
@@ -147,7 +164,8 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol
147164
return nil, err
148165
}
149166

150-
if err = verifyVolumeCompatibility(sizeInGiB, req, share, shareOpts, cs.d.compatOpts, shareTypeCaps); err != nil {
167+
err = verifyVolumeCompatibility(sizeInGiB, req, share, shareOpts, cs.d.compatOpts, shareTypeCaps)
168+
if err != nil {
151169
return nil, status.Errorf(codes.AlreadyExists, "volume %s already exists, but is incompatible with the request: %v", req.GetName(), err)
152170
}
153171

@@ -164,13 +182,6 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol
164182
return nil, status.Errorf(codes.Internal, "failed to grant access to volume %s: %v", share.Name, err)
165183
}
166184

167-
var accessibleTopology []*csi.Topology
168-
if cs.d.withTopology {
169-
// All requisite/preferred topologies are considered valid. Nodes from those zones are required to be able to reach the storage.
170-
// The operator is responsible for making sure that provided topology keys are valid and present on the nodes of the cluster.
171-
accessibleTopology = req.GetAccessibilityRequirements().GetPreferred()
172-
}
173-
174185
volCtx := filterParametersForVolumeContext(params, options.NodeVolumeContextFields())
175186
volCtx["shareID"] = share.ID
176187
volCtx["shareAccessID"] = accessRight.ID

pkg/csi/manila/options/shareoptions.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type ControllerVolumeContext struct {
2424
Protocol string `name:"protocol" matches:"^(?i)CEPHFS|NFS$"`
2525
Type string `name:"type" value:"default:default"`
2626
ShareNetworkID string `name:"shareNetworkID" value:"optional"`
27+
AutoTopology string `name:"autoTopology" value:"default:false" matches:"(?i)^true|false$"`
2728
AvailabilityZone string `name:"availability" value:"optional"`
2829
AppendShareMetadata string `name:"appendShareMetadata" value:"optional"`
2930

pkg/util/util.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import (
66
"fmt"
77
"time"
88

9+
"github.com/container-storage-interface/spec/lib/go/csi"
910
v1 "k8s.io/api/core/v1"
1011
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1112
"k8s.io/apimachinery/pkg/types"
1213
"k8s.io/apimachinery/pkg/util/sets"
1314
"k8s.io/apimachinery/pkg/util/strategicpatch"
1415
clientset "k8s.io/client-go/kubernetes"
16+
"k8s.io/klog/v2"
1517
)
1618

1719
// MyDuration is the encoding.TextUnmarshaler interface for time.Duration
@@ -100,3 +102,28 @@ func PatchService(ctx context.Context, client clientset.Interface, cur, mod *v1.
100102

101103
return nil
102104
}
105+
106+
func GetAZFromTopology(topologyKey string, requirement *csi.TopologyRequirement) string {
107+
var zone string
108+
var exists bool
109+
110+
defer func() { klog.V(1).Infof("detected AZ from the topology: %s", zone) }()
111+
klog.V(4).Infof("preferred topology requirement: %+v", requirement.GetPreferred())
112+
klog.V(4).Infof("requisite topology requirement: %+v", requirement.GetRequisite())
113+
114+
for _, topology := range requirement.GetPreferred() {
115+
zone, exists = topology.GetSegments()[topologyKey]
116+
if exists {
117+
return zone
118+
}
119+
}
120+
121+
for _, topology := range requirement.GetRequisite() {
122+
zone, exists = topology.GetSegments()[topologyKey]
123+
if exists {
124+
return zone
125+
}
126+
}
127+
128+
return zone
129+
}

0 commit comments

Comments
 (0)