Skip to content

Commit 69ede2a

Browse files
committed
feat: add pvecsictl clean command
Add a `pvecsictl clean` command to find orphaned persistent volumes that are no longer referenced by Kubernetes and optionally remove them. Signed-off-by: Serge Logvinov <serge.logvinov@sinextra.dev>
1 parent 1e5279f commit 69ede2a

2 files changed

Lines changed: 196 additions & 0 deletions

File tree

cmd/pvecsictl/clean.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors.
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+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"context"
21+
"errors"
22+
"fmt"
23+
"strings"
24+
25+
cobra "github.com/spf13/cobra"
26+
27+
goproxmox "github.com/sergelogvinov/go-proxmox"
28+
csiconfig "github.com/sergelogvinov/proxmox-csi-plugin/pkg/config"
29+
"github.com/sergelogvinov/proxmox-csi-plugin/pkg/csi"
30+
pxpool "github.com/sergelogvinov/proxmox-csi-plugin/pkg/proxmoxpool"
31+
tools "github.com/sergelogvinov/proxmox-csi-plugin/pkg/tools/kubernetes"
32+
volume "github.com/sergelogvinov/proxmox-csi-plugin/pkg/utils/volume"
33+
34+
rbacv1 "k8s.io/api/authorization/v1"
35+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
36+
clientkubernetes "k8s.io/client-go/kubernetes"
37+
)
38+
39+
type cleanCmd struct {
40+
pclient *pxpool.ProxmoxPool
41+
kclient *clientkubernetes.Clientset
42+
}
43+
44+
func buildCleanCmd() *cobra.Command {
45+
c := &cleanCmd{}
46+
47+
cmd := cobra.Command{
48+
Use: "clean storage-ID proxmox-node",
49+
Aliases: []string{"c"},
50+
Short: "Clean volumes on Proxmox node with specified storage-ID",
51+
Args: cobra.ExactArgs(2),
52+
PreRunE: c.cleanValidate,
53+
RunE: c.runClean,
54+
SilenceUsage: true,
55+
SilenceErrors: true,
56+
}
57+
58+
setCleanCmdFlags(&cmd)
59+
60+
return &cmd
61+
}
62+
63+
func setCleanCmdFlags(cmd *cobra.Command) {
64+
flags := cmd.Flags()
65+
66+
flags.BoolP("force", "f", false, "force delete volumes")
67+
}
68+
69+
// nolint: cyclop, gocyclo
70+
func (c *cleanCmd) runClean(cmd *cobra.Command, args []string) error {
71+
flags := cmd.Flags()
72+
force, _ := flags.GetBool("force") //nolint: errcheck
73+
74+
ctx := context.Background()
75+
storageID := args[0]
76+
node := args[1]
77+
nodeFound := false
78+
79+
for _, region := range c.pclient.GetRegions() {
80+
cl, err := c.pclient.GetProxmoxCluster(region)
81+
if err != nil {
82+
return fmt.Errorf("failed to get Proxmox cluster client for region %s: %v", region, err)
83+
}
84+
85+
if _, err := cl.GetNodeByName(ctx, node); err != nil {
86+
if errors.Is(err, goproxmox.ErrNodeNotFound) {
87+
continue
88+
}
89+
90+
return fmt.Errorf("failed to get node %s on region %s: %v", node, region, err)
91+
}
92+
93+
nodeFound = true
94+
95+
_, err = cl.GetClusterStorage(ctx, storageID)
96+
if err != nil {
97+
return fmt.Errorf("failed to get cluster storage %s on region %s: %v", storageID, region, err)
98+
}
99+
100+
pvs, err := cl.GetStorageContent(ctx, node, storageID)
101+
if err != nil {
102+
return fmt.Errorf("failed to get storage content for storage %s on region %s: %v", storageID, region, err)
103+
}
104+
105+
if len(pvs) == 0 {
106+
return fmt.Errorf("no volumes found on storage %s on region %s", storageID, region)
107+
}
108+
109+
k8sPVs, err := c.kclient.CoreV1().PersistentVolumes().List(ctx, metav1.ListOptions{})
110+
if err != nil {
111+
return fmt.Errorf("failed to list Kubernetes PersistentVolumes: %v", err)
112+
}
113+
114+
for _, proxmoxPV := range pvs {
115+
volName, ok := strings.CutPrefix(proxmoxPV.Volid, storageID+":")
116+
if !ok {
117+
continue
118+
}
119+
120+
vol := volume.NewVolume(region, node, storageID, volName)
121+
if vol.VMID() != "9999" {
122+
continue
123+
}
124+
125+
found := false
126+
127+
for _, k8sPV := range k8sPVs.Items {
128+
if k8sPV.Spec.CSI != nil && k8sPV.Spec.CSI.Driver == csi.DriverName && k8sPV.Spec.CSI.VolumeHandle == vol.VolumeID() {
129+
found = true
130+
131+
break
132+
}
133+
}
134+
135+
if !found {
136+
if force {
137+
fmt.Printf("Delete volume %s with size %dGi on storage %s on region %s\n", vol.Disk(), proxmoxPV.Size/1024/1024/1024, storageID, region)
138+
139+
if err := cl.DeleteVMDisk(ctx, node, storageID, vol.Disk()); err != nil {
140+
return fmt.Errorf("failed to delete volume %s on storage %s on region %s: %v", volName, storageID, region, err)
141+
}
142+
} else {
143+
fmt.Printf("Found unused volume %s with size %dGi on storage %s on region %s\n", vol.Disk(), proxmoxPV.Size/1024/1024/1024, storageID, region)
144+
}
145+
}
146+
}
147+
}
148+
149+
if !nodeFound {
150+
return fmt.Errorf("node %s not found", node)
151+
}
152+
153+
return nil
154+
}
155+
156+
// nolint: dupl,goconst
157+
func (c *cleanCmd) cleanValidate(_ *cobra.Command, _ []string) error {
158+
cfg, err := csiconfig.ReadCloudConfigFromFile(cloudconfig)
159+
if err != nil {
160+
return fmt.Errorf("failed to read config: %v", err)
161+
}
162+
163+
for _, c := range cfg.Clusters {
164+
if c.Username == "" || c.Password == "" {
165+
return fmt.Errorf("this command requires Proxmox root account, please provide username and password in config file (cluster=%s)", c.Region)
166+
}
167+
}
168+
169+
c.pclient, err = pxpool.NewProxmoxPool(cfg.Clusters)
170+
if err != nil {
171+
return fmt.Errorf("failed to create Proxmox cluster client: %v", err)
172+
}
173+
174+
if err = c.pclient.CheckClusters(context.TODO()); err != nil {
175+
return fmt.Errorf("failed to initialize Proxmox clusters: %v", err)
176+
}
177+
178+
kclientConfig, _, err := tools.BuildConfig(kubeconfig, "kube-default")
179+
if err != nil {
180+
return fmt.Errorf("failed to create kubernetes config: %v", err)
181+
}
182+
183+
c.kclient, err = clientkubernetes.NewForConfig(kclientConfig)
184+
if err != nil {
185+
return fmt.Errorf("failed to create kubernetes client: %v", err)
186+
}
187+
188+
accessCheck := []rbacv1.ResourceAttributes{
189+
{Group: "", Namespace: "", Resource: "persistentvolumes", Verb: "get"},
190+
{Group: "", Namespace: "", Resource: "persistentvolumes", Verb: "list"},
191+
{Group: "", Namespace: "", Resource: "nodes", Verb: "get"},
192+
}
193+
194+
return checkPermissions(context.TODO(), c.kclient, accessCheck)
195+
}

cmd/pvecsictl/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ func run() int {
8686
cmd.AddCommand(buildMigrateCmd())
8787
cmd.AddCommand(buildRenameCmd())
8888
cmd.AddCommand(buildSwapCmd())
89+
cmd.AddCommand(buildCleanCmd())
8990

9091
err := cmd.ExecuteContext(ctx)
9192
if err != nil {

0 commit comments

Comments
 (0)