Skip to content

Commit cb994ce

Browse files
authored
Support csi-powerscale remoteAzNetwork parameter in storage class generation (#314)
1 parent db769f3 commit cb994ce

File tree

9 files changed

+281
-11
lines changed

9 files changed

+281
-11
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ vendor/
2727

2828
# Local version files
2929
semver.mk
30+
31+
# Logs
32+
repctl.log

repctl/Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ build-static:
1212
CGO_ENABLED=1 CGO_LDFLAGS="-static" go build ./cmd/repctl
1313

1414
test:
15-
go clean -cache; cd ./pkg; go test -race -cover -coverprofile=coverage.out ./...
15+
cd ./pkg; go test -race -cover -coverprofile=coverage.out ./...
16+
17+
test-sc-generation:
18+
cd ./test/storageclass; go test ./...
1619

1720
# Install Go tools to build the code
1821
tools:

repctl/examples/powerscale_example_values.yaml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ parameters:
2121
accessZone:
2222
source: "System"
2323
target: "System"
24-
# azServiceIP:
25-
# source: "192.168.1.1"
26-
# target: "192.168.1.2"
24+
# azServiceIP:
25+
# source: "192.168.1.1"
26+
# target: "192.168.1.2"
27+
28+
# The CIDR notation of the subnet on which the volumes should be
29+
# accessible. The volumes will only be exported to addresses in
30+
# this subnet. If not specified, then the volume will be exported to
31+
# the default (primary) address of the node.
32+
#
33+
# azNetwork:
34+
# source: 192.168.2.0/24
35+
# target: 192.168.3.0/24

repctl/pkg/cmd/create.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ type Mirrored struct {
4848

4949
// GlobalParameters is a struct that contains different replication configuration parameters for all supported drivers
5050
type GlobalParameters struct {
51-
// PowerStore
52-
ArrayID Mirrored
53-
RemoteSystem Mirrored
51+
// Common to all drivers
5452
Rpo string
55-
IgnoreNamespaces bool
5653
VolumeGroupPrefix string
57-
Mode string
54+
IgnoreNamespaces bool
55+
56+
// PowerStore
57+
ArrayID Mirrored
58+
RemoteSystem Mirrored
59+
Mode string
5860

5961
// PowerMax
6062
Srp Mirrored
@@ -67,6 +69,7 @@ type GlobalParameters struct {
6769
ClusterName Mirrored
6870
AccessZone Mirrored
6971
AzServiceIP Mirrored
72+
AzNetwork Mirrored
7073
IsiPath string
7174
RootClientEnabled Mirrored
7275

repctl/pkg/cmd/templates/isilon_source.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,17 @@ parameters:
2626
{{- if .Parameters.AzServiceIP.Target }}
2727
{{ .ReplicationPrefix }}/remoteAzServiceIP: {{ .Parameters.AzServiceIP.Target }}
2828
{{- end }}
29+
{{- if .Parameters.AzNetwork.Target }}
30+
{{ .ReplicationPrefix }}/remoteAzNetwork: {{ .Parameters.AzNetwork.Target }}
31+
{{- end }}
2932
{{ .ReplicationPrefix }}/remoteRootClientEnabled: "{{ .Parameters.RootClientEnabled.Target }}"
3033
AccessZone: {{ .Parameters.AccessZone.Source }}
3134
{{- if .Parameters.AzServiceIP.Source }}
3235
AzServiceIP: {{ .Parameters.AzServiceIP.Source }}
3336
{{- end }}
37+
{{- if .Parameters.AzNetwork.Source }}
38+
AzNetwork: {{ .Parameters.AzNetwork.Source }}
39+
{{- end }}
3440
IsiPath: {{ .Parameters.IsiPath}}
3541
RootClientEnabled: "{{ .Parameters.RootClientEnabled.Source }}"
36-
ClusterName: {{ .Parameters.ClusterName.Source }}
42+
ClusterName: {{ .Parameters.ClusterName.Source }}

repctl/pkg/cmd/templates/isilon_target.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,17 @@ parameters:
2929
{{- if .Parameters.AzServiceIP.Source }}
3030
{{ .ReplicationPrefix }}/remoteAzServiceIP: {{ .Parameters.AzServiceIP.Source }}
3131
{{- end }}
32+
{{- if .Parameters.AzNetwork.Source }}
33+
{{ .ReplicationPrefix }}/remoteAzNetwork: {{ .Parameters.AzNetwork.Source }}
34+
{{- end }}
3235
{{ .ReplicationPrefix }}/remoteRootClientEnabled: "{{ .Parameters.RootClientEnabled.Source }}"
3336
AccessZone: {{ .Parameters.AccessZone.Target }}
3437
{{- if .Parameters.AzServiceIP.Target }}
3538
AzServiceIP: {{ .Parameters.AzServiceIP.Target }}
3639
{{- end }}
40+
{{- if .Parameters.AzNetwork.Target }}
41+
AzNetwork: {{ .Parameters.AzNetwork.Target }}
42+
{{- end }}
3743
IsiPath: {{ .Parameters.IsiPath}}
3844
RootClientEnabled: "{{ .Parameters.RootClientEnabled.Target }}"
39-
ClusterName: {{ .Parameters.ClusterName.Target }}
45+
ClusterName: {{ .Parameters.ClusterName.Target }}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
Copyright © 2025 Dell Inc. or its subsidiaries. All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License.
13+
*/
14+
15+
package test
16+
17+
import (
18+
"bytes"
19+
"context"
20+
"fmt"
21+
"os"
22+
"os/exec"
23+
"path/filepath"
24+
"testing"
25+
26+
"github.com/stretchr/testify/assert"
27+
)
28+
29+
const (
30+
// The name of the repctl binary.
31+
repctlCLI = "repctl"
32+
// The repctl "create" command used to create k8s resources.
33+
cmdCreate = "create"
34+
// Name of the resource provided to the repctl "create" command for creating
35+
// a storage class.
36+
resourceStorageClass = "sc"
37+
// Option used when creating StorageClasses with repctl that allows
38+
// user to supply a custom repctl config file for generating replication-enabled
39+
// storage classes.
40+
optFromConfig = "--from-config"
41+
// Option provided to repctl to generate output but skip applying.
42+
optDryRun = "--dry-run"
43+
44+
// name of the environment variable used to provide a path to repctl executable.
45+
envRepctlPath = "REPCTL_PATH"
46+
)
47+
48+
// relative location of the directory containing the config files fed to `repctl create sc` command.
49+
var configFileDir = "testdata"
50+
51+
// getRepctlPath searches for a repctl executable and returns a file path to the executable.
52+
// If the executable cannot be found, an error is returned.
53+
// Search paths include csm-replication/repctl, the path provided by environment variable REPCTL_PATH,
54+
// and PATH, in respective order.
55+
func getRepctlPath() (path string, err error) {
56+
fmt.Println("looking for the repctl executable in csm-replication/repctl")
57+
58+
// Try to locate repctl in the default build output location
59+
_, err = os.Stat("../../repctl")
60+
if err == nil {
61+
return "../../repctl", nil
62+
}
63+
fmt.Printf("error looking for repctl in csm-replication/repctl: %s\n", err.Error())
64+
65+
// Try to locate repctl using the path provided via REPCTL_PATH
66+
fmt.Println("looking for repctl using the path provided via REPCTL_PATH")
67+
path = filepath.Join(os.Getenv(envRepctlPath), repctlCLI)
68+
_, err = os.Stat(path)
69+
if err == nil {
70+
return path, nil
71+
}
72+
fmt.Printf("error looking for repctl in REPCTL_PATH: %s\n", err.Error())
73+
74+
// Try to locate repctl as part of one of the paths in PATH
75+
fmt.Println("looking for repctl in PATH")
76+
path, err = exec.LookPath(repctlCLI)
77+
if err == nil {
78+
return path, nil
79+
}
80+
fmt.Printf("error looking for repctl in PATH: %s\n", err.Error())
81+
82+
return "", fmt.Errorf("unable to locate repctl. All methods were exhausted")
83+
}
84+
85+
// use repctl to generate power scale storage class and compare
86+
// the output to the expected output
87+
func TestGeneratePowerScaleStorageClass(t *testing.T) {
88+
repctl, err := getRepctlPath()
89+
if err != nil {
90+
t.Errorf("encountered error while searching for repctl: %s", err.Error())
91+
t.SkipNow()
92+
}
93+
94+
type args struct {
95+
templateName string
96+
}
97+
tests := []struct {
98+
name string
99+
args args
100+
want string
101+
}{
102+
{
103+
name: "generate a csi-powerscale replication storage class with all options populated",
104+
args: args{
105+
templateName: "powerscale",
106+
},
107+
want: powerscaleStorageClass,
108+
},
109+
}
110+
111+
for _, tt := range tests {
112+
configFilePath := filepath.Join(configFileDir, tt.args.templateName+"_tmpl_values.yaml")
113+
generated, err := exec.CommandContext(context.Background(), repctl, cmdCreate, resourceStorageClass, optFromConfig, configFilePath, optDryRun).CombinedOutput()
114+
if err != nil {
115+
t.Errorf("encountered error while generating storage class: %s", err.Error())
116+
t.FailNow()
117+
}
118+
119+
// remove the log message printed by repctl at the beginning of the print out
120+
newlineIndex := bytes.IndexRune(generated, '\n')
121+
generated = generated[newlineIndex:]
122+
123+
assert.Equal(t, string(tt.want), string(generated))
124+
}
125+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
Copyright © 2025 Dell Inc. or its subsidiaries. All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License.
13+
*/
14+
15+
package test
16+
17+
// Expected output when running `repctl create sc --from-config ...` using a powerscale config template.
18+
// Used to validate repctl is generating the expected storage classes.
19+
// If the config file is updated with more configuration options, the new output should be reviewed and
20+
// upon approval, be used to replace these values.
21+
const powerscaleStorageClass = `
22+
# yamllint disable-file
23+
# This file is not valid YAML because it is a Helm template
24+
apiVersion: storage.k8s.io/v1
25+
kind: StorageClass
26+
metadata:
27+
name: isilon-replication
28+
provisioner: csi-isilon.dellemc.com
29+
reclaimPolicy: Delete
30+
volumeBindingMode: Immediate
31+
parameters:
32+
replication.storage.dell.com/isReplicationEnabled: "true"
33+
replication.storage.dell.com/remoteStorageClassName: isilon-replication
34+
replication.storage.dell.com/remoteClusterID: target
35+
replication.storage.dell.com/rpo: Five_Minutes
36+
replication.storage.dell.com/ignoreNamespaces: "false"
37+
replication.storage.dell.com/volumeGroupPrefix: csi
38+
replication.storage.dell.com/remoteSystem: cluster-2
39+
replication.storage.dell.com/remoteRGRetentionPolicy: Retain
40+
replication.storage.dell.com/remotePVRetentionPolicy: Retain
41+
replication.storage.dell.com/remoteAccessZone: System
42+
replication.storage.dell.com/remoteAzServiceIP: 192.168.1.2
43+
replication.storage.dell.com/remoteAzNetwork: 192.168.3.0/24
44+
replication.storage.dell.com/remoteRootClientEnabled: "false"
45+
AccessZone: System
46+
AzServiceIP: 192.168.1.1
47+
AzNetwork: 192.168.2.0/24
48+
IsiPath: /ifs/data/csi
49+
RootClientEnabled: "false"
50+
ClusterName: cluster-1
51+
# yamllint disable-file
52+
# This file is not valid YAML because it is a Helm template
53+
apiVersion: storage.k8s.io/v1
54+
kind: StorageClass
55+
metadata:
56+
name: isilon-replication
57+
provisioner: csi-isilon.dellemc.com
58+
reclaimPolicy: Delete
59+
volumeBindingMode: Immediate
60+
parameters:
61+
replication.storage.dell.com/isReplicationEnabled: "true"
62+
replication.storage.dell.com/remoteStorageClassName: isilon-replication
63+
replication.storage.dell.com/remoteClusterID: source
64+
replication.storage.dell.com/rpo: Five_Minutes
65+
replication.storage.dell.com/ignoreNamespaces: "false"
66+
replication.storage.dell.com/volumeGroupPrefix: csi
67+
replication.storage.dell.com/remoteSystem: cluster-1
68+
replication.storage.dell.com/remoteRGRetentionPolicy: Retain
69+
replication.storage.dell.com/remotePVRetentionPolicy: Retain
70+
replication.storage.dell.com/remoteAccessZone: System
71+
replication.storage.dell.com/remoteAzServiceIP: 192.168.1.1
72+
replication.storage.dell.com/remoteAzNetwork: 192.168.2.0/24
73+
replication.storage.dell.com/remoteRootClientEnabled: "false"
74+
AccessZone: System
75+
AzServiceIP: 192.168.1.2
76+
AzNetwork: 192.168.3.0/24
77+
IsiPath: /ifs/data/csi
78+
RootClientEnabled: "false"
79+
ClusterName: cluster-2
80+
`
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
sourceClusterID: "source"
2+
targetClusterID: "target"
3+
name: "isilon-replication"
4+
driver: "isilon"
5+
reclaimPolicy: "Delete"
6+
replicationPrefix: "replication.storage.dell.com"
7+
remoteRetentionPolicy:
8+
RG: "Retain"
9+
PV: "Retain"
10+
parameters:
11+
rpo: "Five_Minutes"
12+
ignoreNamespaces: "false"
13+
volumeGroupPrefix: "csi"
14+
isiPath: "/ifs/data/csi"
15+
clusterName:
16+
source: "cluster-1"
17+
target: "cluster-2"
18+
rootClientEnabled:
19+
source: "false"
20+
target: "false"
21+
accessZone:
22+
source: "System"
23+
target: "System"
24+
azServiceIP:
25+
source: "192.168.1.1"
26+
target: "192.168.1.2"
27+
28+
# The CIDR notation of the subnet on which the volumes should be
29+
# accessible. The volumes will only be exported to addresses in
30+
# this subnet. If not specified, then the volume will be exported to
31+
# the default (primary) address of the node.
32+
#
33+
azNetwork:
34+
source: 192.168.2.0/24
35+
target: 192.168.3.0/24

0 commit comments

Comments
 (0)