Skip to content

Commit

Permalink
feat: swap pv in already created pvc
Browse files Browse the repository at this point in the history
Imagine you have two PersistentVolumeClaims (PVCs), pvc-a and pvc-b,
each attached to a PersistentVolume (PV), pv-1 and pv-2 respectively.
You want to swap the volumes pv-1 and pv-2 so that pvc-a gets the storage of pv-2 and pvc-b gets the storage of pv-1.

Signed-off-by: Serge Logvinov <[email protected]>
  • Loading branch information
sergelogvinov committed May 4, 2024
1 parent 89adec9 commit 76c899e
Show file tree
Hide file tree
Showing 8 changed files with 430 additions and 36 deletions.
25 changes: 18 additions & 7 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@

# options for analysis running
run:
go: '1.21'
# default concurrency is a available CPU number
# concurrency: 4

# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 10m
timeout: 10m

# exit code when at least one issue was found, default is 1
issues-exit-code: 1
Expand All @@ -20,7 +19,7 @@ run:
# won't be reported. Default value is empty list, but there is
# no need to include all autogenerated files, we confidently recognize
# autogenerated files. If it's not please let us know.
skip-files:
exclude-files:
- charts/
- docs/

Expand All @@ -35,7 +34,13 @@ run:
# output configuration options
output:
# colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number"
format: line-number
formats:
- format: line-number
path: stdout
print-issued-lines: true
print-linter-name: true
uniq-by-line: true
sort-results: true

# all available settings of specific linters
linters-settings:
Expand All @@ -47,9 +52,7 @@ linters-settings:
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: true
govet:
# report about shadowed variables
check-shadowing: true
govet: {}
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
Expand Down Expand Up @@ -168,11 +171,19 @@ linters:
- perfsprint
- protogetter

# temporarily disabled linters
- copyloopvar

# abandoned linters for which golangci shows the warning that the repo is archived by the owner
- golint
- interfacer
- maligned
- scopelint
- varcheck
- structcheck
- deadcode
- ifshort
- perfsprint

disable-all: false
fast: false
Expand Down
1 change: 1 addition & 0 deletions cmd/pvecsictl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func run() int {

cmd.AddCommand(buildMigrateCmd())
cmd.AddCommand(buildRenameCmd())
cmd.AddCommand(buildSwapCmd())

err := cmd.ExecuteContext(ctx)
if err != nil {
Expand Down
42 changes: 20 additions & 22 deletions cmd/pvecsictl/rename.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,9 @@ func buildRenameCmd() *cobra.Command {
func setrenameCmdFlags(cmd *cobra.Command) {
flags := cmd.Flags()

flags.StringP("namespace", "n", "", "namespace of the persistentvolumeclaims")
flags.StringP("namespace", "n", "", "namespace of the PersistentVolumeClaims")

flags.BoolP("force", "f", false, "force migration even if the persistentvolumeclaims is in use")
flags.Int("timeout", 120, "task timeout in seconds")
flags.BoolP("force", "f", false, "force migration even if the PersistentVolumeClaims is in use")
}

// nolint: cyclop, gocyclo
Expand All @@ -84,25 +83,30 @@ func (c *renameCmd) runRename(cmd *cobra.Command, args []string) error {

cordonedNodes := []string{}

if len(pods) > 0 {
if force {
logger.Infof("persistentvolumeclaims is using by pods: %s on node %s, trying to force migration\n", strings.Join(pods, ","), vmName)

var csiNodes []string
defer func() {
if len(cordonedNodes) > 0 {
logger.Infof("uncordoning nodes: %s", strings.Join(cordonedNodes, ","))

csiNodes, err = tools.CSINodes(ctx, c.kclient, srcPV.Spec.CSI.Driver)
if err != nil {
return err
if err = tools.UncondonNodes(ctx, c.kclient, cordonedNodes); err != nil {
logger.Errorf("failed to uncordon nodes: %v", err)
}
}
}()

cordonedNodes = append(cordonedNodes, csiNodes...)
if len(pods) > 0 {
if force {
if srcPV.Spec.CSI == nil {
return fmt.Errorf("only CSI PersistentVolumes can be swapped in force mode")
}

logger.Infof("cordoning nodes: %s", strings.Join(cordonedNodes, ","))
logger.Infof("persistentvolumeclaims is using by pods: %s on node %s, trying to force migration\n", strings.Join(pods, ","), vmName)

if _, err = tools.CondonNodes(ctx, c.kclient, cordonedNodes); err != nil {
cordonedNodes, err = cordoneNodeWithPVs(ctx, c.kclient, srcPV)
if err != nil {
return fmt.Errorf("failed to cordon nodes: %v", err)
}

logger.Infof("cordoned nodes: %s", strings.Join(cordonedNodes, ","))
logger.Infof("terminated pods: %s", strings.Join(pods, ","))

for _, pod := range pods {
Expand Down Expand Up @@ -134,15 +138,9 @@ func (c *renameCmd) runRename(cmd *cobra.Command, args []string) error {

err = renamePVC(ctx, c.kclient, c.namespace, srcPVC, srcPV, args[1])
if err != nil {
return fmt.Errorf("failed to rename persistentvolumeclaims: %v", err)
}

if force {
logger.Infof("uncordoning nodes: %s", strings.Join(cordonedNodes, ","))
cordonedNodes = []string{}

if err = tools.UncondonNodes(ctx, c.kclient, cordonedNodes); err != nil {
return fmt.Errorf("failed to uncordon nodes: %v", err)
}
return fmt.Errorf("failed to rename persistentvolumeclaims: %v", err)
}

logger.Infof("persistentvolumeclaims %s has been renamed", args[0])
Expand Down
218 changes: 218 additions & 0 deletions cmd/pvecsictl/swap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/*
Copyright 2023 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 main

import (
"context"
"fmt"
"strings"
"time"

cobra "github.com/spf13/cobra"

tools "github.com/sergelogvinov/proxmox-csi-plugin/pkg/tools"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientkubernetes "k8s.io/client-go/kubernetes"
)

type swapCmd struct {
kclient *clientkubernetes.Clientset
namespace string
}

func buildSwapCmd() *cobra.Command {
c := &swapCmd{}

cmd := cobra.Command{
Use: "swap pvc-a pvc-b",
Aliases: []string{"sw"},
Short: "Swap PersistentVolumes between two PersistentVolumeClaims",
Args: cobra.ExactArgs(2),
PreRunE: c.swapValidate,
RunE: c.runSwap,
SilenceUsage: true,
SilenceErrors: true,
}

setSwapCmdFlags(&cmd)

return &cmd
}

func setSwapCmdFlags(cmd *cobra.Command) {
flags := cmd.Flags()

flags.StringP("namespace", "n", "", "namespace of the PersistentVolumeClaims")

flags.BoolP("force", "f", false, "force migration even if the PersistentVolumeClaims is in use")
}

// nolint: cyclop, gocyclo
func (c *swapCmd) runSwap(cmd *cobra.Command, args []string) error {
flags := cmd.Flags()
force, _ := flags.GetBool("force") //nolint: errcheck

var err error

ctx := context.Background()

srcPVC, srcPV, err := tools.PVCResources(ctx, c.kclient, c.namespace, args[0])
if err != nil {
return fmt.Errorf("failed to get resources: %v", err)
}

srcPods, srcVMName, err := tools.PVCPodUsage(ctx, c.kclient, c.namespace, args[0])
if err != nil {
return fmt.Errorf("failed to find pods using pvc: %v", err)
}

dstPVC, dstPV, err := tools.PVCResources(ctx, c.kclient, c.namespace, args[1])
if err != nil {
return fmt.Errorf("failed to get resources: %v", err)
}

dstPods, dstVMName, err := tools.PVCPodUsage(ctx, c.kclient, c.namespace, args[1])
if err != nil {
return fmt.Errorf("failed to find pods using pvc: %v", err)
}

cordonedNodes := []string{}

defer func() {
if len(cordonedNodes) > 0 {
logger.Infof("uncordoning nodes: %s", strings.Join(cordonedNodes, ","))

if err = tools.UncondonNodes(ctx, c.kclient, cordonedNodes); err != nil {
logger.Errorf("failed to uncordon nodes: %v", err)
}
}
}()

if len(srcPods) > 0 || len(dstPods) > 0 {
if force {
var csiNodes []string

if srcPV.Spec.CSI == nil || dstPV.Spec.CSI == nil {
return fmt.Errorf("only CSI PersistentVolumes can be swapped in force mode")
}

if len(srcPods) > 0 {
logger.Infof("persistentvolumeclaims is using by pods: %s on node %s, trying to force swap\n", strings.Join(srcPods, ","), srcVMName)

csiNodes, err = cordoneNodeWithPVs(ctx, c.kclient, srcPV)
if err != nil {
return fmt.Errorf("failed to cordon nodes: %v", err)
}

cordonedNodes = append(cordonedNodes, csiNodes...)
}

if len(dstPods) > 0 {
logger.Infof("persistentvolumeclaims is using by pods: %s on node %s, trying to force swap\n", strings.Join(dstPods, ","), dstVMName)

csiNodes, err = cordoneNodeWithPVs(ctx, c.kclient, dstPV)
if err != nil {
return fmt.Errorf("failed to cordon nodes: %v", err)
}

cordonedNodes = append(cordonedNodes, csiNodes...)
}

logger.Infof("cordoned nodes: %s", strings.Join(cordonedNodes, ","))

pods := srcPods
pods = append(pods, dstPods...)

logger.Infof("terminated pods: %s", strings.Join(pods, ","))

for _, pod := range pods {
if err = c.kclient.CoreV1().Pods(c.namespace).Delete(ctx, pod, metav1.DeleteOptions{}); err != nil {
return fmt.Errorf("failed to delete pod: %v", err)
}
}

waitPods := func(pod string) error {
for {
p, _, e := tools.PVCPodUsage(ctx, c.kclient, c.namespace, pod)
if e != nil {
return fmt.Errorf("failed to find pods using pvc: %v", e)
}

if len(p) == 0 {
break
}

logger.Infof("waiting pods: %s", strings.Join(p, " "))

time.Sleep(2 * time.Second)
}

return nil
}

if err = waitPods(args[0]); err != nil {
return err
}

if err = waitPods(args[1]); err != nil {
return err
}

time.Sleep(5 * time.Second)
} else {
if len(srcPods) > 0 {
return fmt.Errorf("persistentvolumeclaims is using by pods: %s on the node %s, cannot swap pvc", strings.Join(srcPods, ","), srcVMName)
}

if len(dstPods) > 0 {
return fmt.Errorf("persistentvolumeclaims is using by pods: %s on the node %s, cannot swap pvc", strings.Join(dstPods, ","), dstVMName)
}
}
}

err = swapPVC(ctx, c.kclient, c.namespace, srcPVC, srcPV, dstPVC, dstPV)
if err != nil {
cordonedNodes = []string{}

return fmt.Errorf("failed to swap persistentvolumeclaims: %v", err)
}

logger.Infof("persistentvolumeclaims %s,%s has been swapped", args[0], args[1])

return nil
}

func (c *swapCmd) swapValidate(cmd *cobra.Command, _ []string) error {
flags := cmd.Flags()

namespace, _ := flags.GetString("namespace") //nolint: errcheck

kclientConfig, namespace, err := tools.BuildConfig(kubeconfig, namespace)
if err != nil {
return fmt.Errorf("failed to create kubernetes config: %v", err)
}

c.kclient, err = clientkubernetes.NewForConfig(kclientConfig)
if err != nil {
return fmt.Errorf("failed to create kubernetes client: %v", err)
}

c.namespace = namespace

return nil
}
Loading

0 comments on commit 76c899e

Please sign in to comment.