Skip to content

Commit e0a6d7f

Browse files
jharrodclintonkvivakeramjwebster7reederc42
authored
Iscsi vol move
Adds ONTAP SAN volume move support to Trident. Co-authored-by: Clinton Knight <cknight@netapp.com> Co-authored-by: Clinton Knight <clintonk@users.noreply.github.com> Co-authored-by: vivake <66435586+vivakeram@users.noreply.github.com> Co-authored-by: Joe Webster <31218426+jwebster7@users.noreply.github.com> Co-authored-by: reederc42 <reederc42@gmail.com> Co-authored-by: Joe Webster <websterj@netapp.com> Co-authored-by: Tori Revilla <52927195+torirevilla@users.noreply.github.com> Co-authored-by: torirevilla <victorir@netapp.com> Co-authored-by: Keir Jackson <keirj@netapp.com>
1 parent 1ed9f2c commit e0a6d7f

150 files changed

Lines changed: 11096 additions & 763 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cli/cmd/create_volume_move.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// Copyright 2026 NetApp, Inc. All Rights Reserved.
2+
3+
package cmd
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"time"
9+
10+
"github.com/spf13/cobra"
11+
apierrors "k8s.io/apimachinery/pkg/api/errors"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
14+
k8sclient "github.com/netapp/trident/cli/k8s_client"
15+
netappv1 "github.com/netapp/trident/persistent_store/crd/apis/netapp/v1"
16+
tridentclientset "github.com/netapp/trident/persistent_store/crd/client/clientset/versioned"
17+
"github.com/netapp/trident/utils/errors"
18+
)
19+
20+
const createVolumeMoveTimeout = 30 * time.Second
21+
22+
// createVolumeMoveCreateK8SClients creates Kubernetes clients for volume-move; tests may replace it.
23+
var createVolumeMoveCreateK8SClients = k8sclient.CreateK8SClients
24+
25+
// createVolumeMoveTridentClientOverride, if set, is used for TridentVolumeMove Create instead of clients.TridentClient (tests).
26+
var createVolumeMoveTridentClientOverride tridentclientset.Interface
27+
28+
var (
29+
volume string
30+
volumeMoveTargetPool string
31+
volumeMoveTargetNode string
32+
volumeMoveSrcNode string
33+
volumeMoveSrcPool string
34+
volumeMoveDeleteAfterSuccess string
35+
)
36+
37+
func init() {
38+
createCmd.AddCommand(createVolumeMoveCmd)
39+
createVolumeMoveCmd.Flags().StringVar(&volume, "volume", "",
40+
"Name of the volume to move (also used as the TridentVolumeMove CR name)")
41+
createVolumeMoveCmd.Flags().StringVar(&volumeMoveTargetPool, "target-pool", "",
42+
"Name of the destination storage pool for the volume")
43+
createVolumeMoveCmd.Flags().StringVar(&volumeMoveSrcPool, "source-pool", "",
44+
"Name of the source storage pool for the volume")
45+
createVolumeMoveCmd.Flags().StringVar(&volumeMoveTargetNode, "target-node", "",
46+
"Name of the destination ONTAP node")
47+
createVolumeMoveCmd.Flags().StringVar(&volumeMoveSrcNode, "source-node", "",
48+
"Name of the source ONTAP node")
49+
createVolumeMoveCmd.Flags().StringVar(&volumeMoveDeleteAfterSuccess, "delete-after-success", "",
50+
"When set, delete this CR after the given duration once the move succeeds (for example 10m or 30s); "+
51+
"0 deletes immediately; omit to retain the CR (failed moves are not deleted)")
52+
53+
_ = createVolumeMoveCmd.MarkFlagRequired("volume")
54+
_ = createVolumeMoveCmd.MarkFlagRequired("target-pool")
55+
_ = createVolumeMoveCmd.MarkFlagRequired("source-pool")
56+
_ = createVolumeMoveCmd.MarkFlagRequired("target-node")
57+
_ = createVolumeMoveCmd.MarkFlagRequired("source-node")
58+
}
59+
60+
var createVolumeMoveCmd = &cobra.Command{
61+
Use: "volume-move",
62+
Aliases: []string{"tvm"},
63+
Short: "Start a volume move by creating a Kubernetes TridentVolumeMove CR",
64+
Args: cobra.NoArgs,
65+
RunE: func(cmd *cobra.Command, args []string) error {
66+
// Parse the duration to ensure it's valid
67+
deleteAfterSuccessDuration, err := parseVolumeMoveDeleteAfterSuccess(volumeMoveDeleteAfterSuccess)
68+
if err != nil {
69+
return err
70+
}
71+
if OperatingMode == ModeTunnel {
72+
tunnelArgs := []string{
73+
"create",
74+
"volume-move",
75+
"--volume", volume,
76+
"--target-pool", volumeMoveTargetPool,
77+
"--target-node", volumeMoveTargetNode,
78+
"--source-node", volumeMoveSrcNode,
79+
"--source-pool", volumeMoveSrcPool,
80+
}
81+
if volumeMoveDeleteAfterSuccess != "" {
82+
tunnelArgs = append(tunnelArgs, "--delete-after-success", volumeMoveDeleteAfterSuccess)
83+
}
84+
out, err := TunnelCommand(tunnelArgs)
85+
printOutput(cmd, out, err)
86+
return err
87+
}
88+
ctx, cancel := context.WithTimeout(cmd.Context(), createVolumeMoveTimeout)
89+
defer cancel()
90+
return createTridentVolumeMove(ctx, deleteAfterSuccessDuration)
91+
},
92+
}
93+
94+
// parseVolumeMoveDeleteAfterSuccess parses a Go duration string for deleteAfterSuccess.
95+
// An empty string means the field is omitted on the CR.
96+
func parseVolumeMoveDeleteAfterSuccess(durationStr string) (*metav1.Duration, error) {
97+
if durationStr == "" {
98+
return nil, nil
99+
}
100+
duration, err := time.ParseDuration(durationStr)
101+
if err != nil {
102+
return nil, fmt.Errorf("invalid --delete-after-success duration %q: %w", durationStr, err)
103+
}
104+
if duration < 0 {
105+
return nil, fmt.Errorf("invalid --delete-after-success duration %q: must not be negative", durationStr)
106+
}
107+
return &metav1.Duration{Duration: duration}, nil
108+
}
109+
110+
func buildTridentVolumeMove(deleteAfterSuccessDuration *metav1.Duration) *netappv1.TridentVolumeMove {
111+
spec := netappv1.TridentVolumeMoveSpec{
112+
TargetPool: volumeMoveTargetPool,
113+
SourcePool: volumeMoveSrcPool,
114+
TargetNode: volumeMoveTargetNode,
115+
SourceNode: volumeMoveSrcNode,
116+
}
117+
if deleteAfterSuccessDuration != nil {
118+
spec.DeleteAfterSuccess = deleteAfterSuccessDuration
119+
}
120+
121+
return &netappv1.TridentVolumeMove{
122+
TypeMeta: metav1.TypeMeta{
123+
APIVersion: netappv1.SchemeGroupVersion.String(),
124+
Kind: "TridentVolumeMove",
125+
},
126+
ObjectMeta: metav1.ObjectMeta{
127+
Name: volume,
128+
},
129+
Spec: spec,
130+
}
131+
}
132+
133+
func createTridentVolumeMove(ctx context.Context, deleteAfterSuccessDuration *metav1.Duration) error {
134+
tvm := buildTridentVolumeMove(deleteAfterSuccessDuration)
135+
136+
clients, err := createVolumeMoveCreateK8SClients("", KubeConfigPath, TridentPodNamespace)
137+
if err != nil {
138+
return err
139+
}
140+
141+
clients.K8SClient.SetTimeout(k8sTimeout)
142+
143+
namespace := clients.Namespace
144+
if namespace == "" {
145+
return errors.New("namespace is required (use tridentctl -n or a kube context with a default namespace)")
146+
}
147+
148+
trident := createVolumeMoveTridentClientOverride
149+
if trident == nil {
150+
trident = clients.TridentClient
151+
}
152+
created, err := trident.TridentV1().TridentVolumeMoves(namespace).Create(
153+
ctx, tvm, metav1.CreateOptions{})
154+
if err != nil {
155+
if apierrors.IsAlreadyExists(err) {
156+
return fmt.Errorf("TridentVolumeMove %q already exists in namespace %s", tvm.Name, namespace)
157+
}
158+
return err
159+
}
160+
161+
WriteVolumeMove(created)
162+
return nil
163+
}

0 commit comments

Comments
 (0)