Skip to content

Commit 2fc37d5

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 2fc37d5

3 files changed

Lines changed: 214 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 {

docs/pvecsictl.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,31 @@ Usage:
4848
pvecsictl [command]
4949

5050
Available Commands:
51+
clean List Proxmox volumes and check if they exist in Kubernetes PVs
5152
migrate Migrate data from one Proxmox node to another
5253
rename Rename PersistentVolumeClaim
5354
swap Swap PersistentVolumes between two PersistentVolumeClaims
5455
```
5556
5657
## Commands
5758
59+
### Clean
60+
61+
List all Proxmox volumes in the specified storage and check if they exist in Kubernetes PersistentVolumes.
62+
63+
```shell
64+
pvecsictl clean --config=clusters.yaml storage-ID proxmox-node
65+
```
66+
67+
Example output:
68+
69+
```shell
70+
$ pvecsictl clean --config=clusters.yaml data worker-1
71+
Found unused volume vm-9999-pvc-5901c1d7-395f-4730-b75a-f971b26d12d2 with size 50Gi on storage data on region homelab
72+
Found unused volume vm-9999-pvc-5bbb49f6-0789-4323-9ac3-d87f1c0e491e with size 50Gi on storage data on region homelab
73+
Found unused volume vm-9999-pvc-a7aae4d6-36bd-4fc6-83d2-55c0aa26f45a with size 1Gi on storage data on region homelab
74+
```
75+
5876
### Migrate
5977

6078
Migration requires root privileges on the Proxmox cluster.

0 commit comments

Comments
 (0)