From b19baa3431db9a090e78e0b53f48f17253b2ba92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Thu, 30 May 2024 00:45:10 -0400 Subject: [PATCH] incusd/api_project: Add force deletion logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #245 Signed-off-by: Stéphane Graber --- cmd/incusd/api_project.go | 245 +++++++++++++++++++++++++++++++++++++- 1 file changed, 242 insertions(+), 3 deletions(-) diff --git a/cmd/incusd/api_project.go b/cmd/incusd/api_project.go index 5225e4f6c44..96757f7f042 100644 --- a/cmd/incusd/api_project.go +++ b/cmd/incusd/api_project.go @@ -14,6 +14,7 @@ import ( "github.com/gorilla/mux" + "github.com/lxc/incus/v6/client" "github.com/lxc/incus/v6/internal/jmap" "github.com/lxc/incus/v6/internal/server/auth" "github.com/lxc/incus/v6/internal/server/db" @@ -917,6 +918,7 @@ func projectDelete(d *Daemon, r *http.Request) response.Response { } var id int64 + var usedBy []string err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { project, err := cluster.GetProject(ctx, tx.Tx(), name) if err != nil { @@ -933,8 +935,10 @@ func projectDelete(d *Daemon, r *http.Request) response.Response { return fmt.Errorf("Only empty projects can be removed.") } } else { - // WIP: delete everything - logger.Errorf("WIP") + usedBy, err = projectUsedBy(ctx, tx, project) + if err != nil { + return err + } } id, err = cluster.GetProjectID(ctx, tx.Tx(), name) @@ -942,9 +946,244 @@ func projectDelete(d *Daemon, r *http.Request) response.Response { return fmt.Errorf("Fetch project id %q: %w", name, err) } - return cluster.DeleteProject(ctx, tx.Tx(), name) + return nil }) + if err != nil { + return response.SmartError(err) + } + + // Handle requests to empty the project. + if force { + // Parse used by list. + defaultProfile := api.NewURL().Path(version.APIVersion, "profiles", api.ProjectDefaultName).Project(name).String() + entries := map[string][]string{} + var count int + + for _, u := range usedBy { + // Skip the default profile. + if u == defaultProfile { + continue + } + + // Parse the URL. + uri, err := url.Parse(u) + if err != nil { + return response.InternalError(err) + } + + elements := strings.Split(uri.Path, "/") + if len(elements) < 4 { + return response.InternalError(fmt.Errorf("Bad usedBy entry: %s", u)) + } + + if elements[2] == "storage-pools" { + if elements[4] == "buckets" { + if entries["storage-buckets"] == nil { + entries["storage-buckets"] = []string{} + } + + entry := fmt.Sprintf("%s/%s", elements[3], elements[5]) + target := uri.Query().Get("target") + if target != "" { + entry = fmt.Sprintf("%s/%s", entry, target) + } + + entries["storage-buckets"] = append(entries["storage-buckets"], entry) + } else if elements[4] == "volumes" { + if entries["storage-volumes"] == nil { + entries["storage-volumes"] = []string{} + } + + entry := fmt.Sprintf("%s/%s", elements[3], elements[6]) + target := uri.Query().Get("target") + if target != "" { + entry = fmt.Sprintf("%s/%s", entry, target) + } + + entries["storage-volumes"] = append(entries["storage-volumes"], entry) + } + } else { + if entries[elements[2]] == nil { + entries[elements[2]] = []string{} + } + + entries[elements[2]] = append(entries[elements[2]], elements[3]) + } + + count++ + } + + // Connect to the local server. + target, err := incus.ConnectIncusUnix(s.OS.GetUnixSocket(), nil) + if err != nil { + return response.InternalError(err) + } + + target = target.UseProject(name) + + // Delete instances. + for _, instName := range entries["instances"] { + // Get current instance state. + instState, _, err := target.GetInstance(instName) + if err != nil { + return response.InternalError(err) + } + + // If running, force stop it. + if instState.StatusCode != api.Stopped { + req := api.InstanceStatePut{ + Action: "stop", + Timeout: -1, + Force: true, + } + + op, err := target.UpdateInstanceState(instName, req, "") + if err != nil { + return response.InternalError(err) + } + + err = op.Wait() + if err != nil { + return response.InternalError(err) + } + } + + // Get the instance configuration. + inst, _, err := target.GetInstance(instName) + if err != nil { + return response.InternalError(err) + } + + // Clear security.protection.delete if set. + if util.IsTrue(inst.ExpandedConfig["security.protection.delete"]) { + inst.Config["security.protection.delete"] = "false" + op, err := target.UpdateInstance(instName, inst.Writable(), "") + if err != nil { + return response.InternalError(err) + } + + err = op.Wait() + if err != nil { + return response.InternalError(err) + } + } + + // Delete the instance. + op, err := target.DeleteInstance(instName) + if err != nil { + return response.InternalError(err) + } + err = op.Wait() + if err != nil { + return response.InternalError(err) + } + + // Done deleting the instance. + count-- + } + + // Delete profiles. + for _, profileName := range entries["profiles"] { + err := target.DeleteProfile(profileName) + if err != nil { + return response.InternalError(err) + } + + // Done deleting the profile. + count-- + } + + // Delete images. + for _, imageFingerprint := range entries["images"] { + op, err := target.DeleteImage(imageFingerprint) + if err != nil { + return response.InternalError(err) + } + + err = op.Wait() + if err != nil { + return response.InternalError(err) + } + + // Done deleting the image. + count-- + } + + // Delete networks. + for _, networkName := range entries["networks"] { + err := target.DeleteNetwork(networkName) + if err != nil { + return response.InternalError(err) + } + + // Done deleting the network. + count-- + } + + // Delete network ACLs. + for _, networkACLName := range entries["network-acls"] { + err := target.DeleteNetworkACL(networkACLName) + if err != nil { + return response.InternalError(err) + } + + // Done deleting the network ACL. + count-- + } + + // Delete network zones. + for _, networkZoneName := range entries["network-zones"] { + err := target.DeleteNetworkZone(networkZoneName) + if err != nil { + return response.InternalError(err) + } + + // Done deleting the network zone. + count-- + } + + // Delete storage volumes. + for _, volume := range entries["storage-volumes"] { + fields := strings.Split(volume, "/") + if len(fields) == 3 { + target.UseTarget(fields[2]) + } + + err := target.DeleteStoragePoolVolume(fields[0], "custom", fields[1]) + if err != nil { + return response.InternalError(err) + } + + // Done deleting the storage volume. + count-- + } + + // Delete storage buckets. + for _, volume := range entries["storage-buckets"] { + fields := strings.Split(volume, "/") + if len(fields) == 3 { + target.UseTarget(fields[2]) + } + + err := target.DeleteStoragePoolBucket(fields[0], fields[1]) + if err != nil { + return response.InternalError(err) + } + + // Done deleting the storage volume. + count-- + } + + // Check if anything is left. + if count != 0 { + return response.BadRequest(fmt.Errorf("Project couldn't be automatically emptied")) + } + } + + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + return cluster.DeleteProject(ctx, tx.Tx(), name) + }) if err != nil { return response.SmartError(err) }