diff --git a/lxd/cluster/connect.go b/lxd/cluster/connect.go index ecfe482615aa..37705cdb31f1 100644 --- a/lxd/cluster/connect.go +++ b/lxd/cluster/connect.go @@ -16,7 +16,6 @@ import ( "github.com/canonical/lxd/lxd/instance/instancetype" "github.com/canonical/lxd/lxd/request" "github.com/canonical/lxd/lxd/state" - storagePools "github.com/canonical/lxd/lxd/storage" "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/api" "github.com/canonical/lxd/shared/version" @@ -121,86 +120,6 @@ func ConnectIfInstanceIsRemote(s *state.State, projectName string, instName stri return client, nil } -// ConnectIfVolumeIsRemote figures out the address of the cluster member on which the volume with the given name is -// defined. If it's not the local cluster member it will connect to it and return the connected client, otherwise -// it just returns nil. If there is more than one cluster member with a matching volume name, an error is returned. -func ConnectIfVolumeIsRemote(s *state.State, poolName string, projectName string, volumeName string, volumeType int, networkCert *shared.CertInfo, serverCert *shared.CertInfo, r *http.Request) (lxd.InstanceServer, error) { - localNodeID := s.DB.Cluster.GetNodeID() - var err error - var nodes []db.NodeInfo - var poolID int64 - err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - poolID, err = tx.GetStoragePoolID(ctx, poolName) - if err != nil { - return err - } - - nodes, err = tx.GetStorageVolumeNodes(ctx, poolID, projectName, volumeName, volumeType) - if err != nil { - return err - } - - return nil - }) - if err != nil && err != db.ErrNoClusterMember { - return nil, err - } - - // If volume uses a remote storage driver and so has no explicit cluster member, then we need to check - // whether it is exclusively attached to remote instance, and if so then we need to forward the request to - // the node whereit is currently used. This avoids conflicting with another member when using it locally. - if err == db.ErrNoClusterMember { - // GetStoragePoolVolume returns a volume with an empty Location field for remote drivers. - var dbVolume *db.StorageVolume - err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - dbVolume, err = tx.GetStoragePoolVolume(ctx, poolID, projectName, volumeType, volumeName, true) - return err - }) - if err != nil { - return nil, err - } - - remoteInstance, err := storagePools.VolumeUsedByExclusiveRemoteInstancesWithProfiles(s, poolName, projectName, &dbVolume.StorageVolume) - if err != nil { - return nil, fmt.Errorf("Failed checking if volume %q is available: %w", volumeName, err) - } - - if remoteInstance == nil { - // Volume isn't exclusively attached to an instance. Use local cluster member. - return nil, nil - } - - var instNode db.NodeInfo - err = s.DB.Cluster.Transaction(s.ShutdownCtx, func(ctx context.Context, tx *db.ClusterTx) error { - instNode, err = tx.GetNodeByName(ctx, remoteInstance.Node) - return err - }) - if err != nil { - return nil, fmt.Errorf("Failed getting cluster member info for %q: %w", remoteInstance.Node, err) - } - - // Replace node list with instance's cluster member node (which might be local member). - nodes = []db.NodeInfo{instNode} - } - - nodeCount := len(nodes) - if nodeCount > 1 { - return nil, fmt.Errorf("More than one cluster member has a volume named %q. Please target a specific member", volumeName) - } else if nodeCount < 1 { - // Should never get here. - return nil, fmt.Errorf("Volume %q has empty cluster member list", volumeName) - } - - node := nodes[0] - if node.ID == localNodeID { - // Use local cluster member if volume belongs to this local member. - return nil, nil - } - - // Connect to remote cluster member. - return Connect(node.Address, networkCert, serverCert, r, false) -} - // SetupTrust is a convenience around InstanceServer.CreateCertificate that adds the given server certificate to // the trusted pool of the cluster at the given address, using the given token. The certificate is added as // type CertificateTypeServer to allow intra-member communication. If a certificate with the same fingerprint diff --git a/lxd/response.go b/lxd/response.go index 68e843b8812c..8f056f21bbc8 100644 --- a/lxd/response.go +++ b/lxd/response.go @@ -57,26 +57,21 @@ func forwardedResponseIfInstanceIsRemote(s *state.State, r *http.Request, projec return response.ForwardedResponse(client, r), nil } -// forwardedResponseIfVolumeIsRemote redirects a request to the node hosting -// the volume with the given pool ID, name and type. If the container is local, -// nothing gets done and nil is returned. If more than one node has a matching -// volume, an error is returned. -// -// This is used when no targetNode is specified, and saves users some typing -// when the volume name/type is unique to a node. -func forwardedResponseIfVolumeIsRemote(s *state.State, r *http.Request, poolName string, projectName string, volumeName string, volumeType int) response.Response { - if request.QueryParam(r, "target") != "" { +// forwardedResponseIfVolumeIsRemote checks for the presence of the ctxStorageVolumeRemoteNodeInfo key in the request context. +// If it is present, the db.NodeInfo value for this key is used to set up a client for the indicated member and forward the request. +// Otherwise, a nil response is returned to indicate that the request was not forwarded, and should continue within this member. +func forwardedResponseIfVolumeIsRemote(s *state.State, r *http.Request) response.Response { + storageVolumeDetails, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) + if err != nil { + return nil + } else if storageVolumeDetails.forwardingNodeInfo == nil { return nil } - client, err := cluster.ConnectIfVolumeIsRemote(s, poolName, projectName, volumeName, volumeType, s.Endpoints.NetworkCert(), s.ServerCert(), r) + client, err := cluster.Connect(storageVolumeDetails.forwardingNodeInfo.Address, s.Endpoints.NetworkCert(), s.ServerCert(), r, false) if err != nil { return response.SmartError(err) } - if client == nil { - return nil - } - return response.ForwardedResponse(client, r) } diff --git a/lxd/storage_buckets.go b/lxd/storage_buckets.go index 19c10ca0f46f..53f74c43dead 100644 --- a/lxd/storage_buckets.go +++ b/lxd/storage_buckets.go @@ -34,25 +34,59 @@ var storagePoolBucketsCmd = APIEndpoint{ var storagePoolBucketCmd = APIEndpoint{ Path: "storage-pools/{poolName}/buckets/{bucketName}", - Delete: APIEndpointAction{Handler: storagePoolBucketDelete, AccessHandler: allowPermission(entity.TypeStorageBucket, auth.EntitlementCanDelete, "poolName", "bucketName")}, - Get: APIEndpointAction{Handler: storagePoolBucketGet, AccessHandler: allowPermission(entity.TypeStorageBucket, auth.EntitlementCanView, "poolName", "bucketName")}, - Patch: APIEndpointAction{Handler: storagePoolBucketPut, AccessHandler: allowPermission(entity.TypeStorageBucket, auth.EntitlementCanEdit, "poolName", "bucketName")}, - Put: APIEndpointAction{Handler: storagePoolBucketPut, AccessHandler: allowPermission(entity.TypeStorageBucket, auth.EntitlementCanEdit, "poolName", "bucketName")}, + Delete: APIEndpointAction{Handler: storagePoolBucketDelete, AccessHandler: storageBucketAccessHandler(auth.EntitlementCanDelete)}, + Get: APIEndpointAction{Handler: storagePoolBucketGet, AccessHandler: storageBucketAccessHandler(auth.EntitlementCanView)}, + Patch: APIEndpointAction{Handler: storagePoolBucketPut, AccessHandler: storageBucketAccessHandler(auth.EntitlementCanEdit)}, + Put: APIEndpointAction{Handler: storagePoolBucketPut, AccessHandler: storageBucketAccessHandler(auth.EntitlementCanEdit)}, } var storagePoolBucketKeysCmd = APIEndpoint{ Path: "storage-pools/{poolName}/buckets/{bucketName}/keys", - Get: APIEndpointAction{Handler: storagePoolBucketKeysGet, AccessHandler: allowPermission(entity.TypeStorageBucket, auth.EntitlementCanView, "poolName", "bucketName")}, - Post: APIEndpointAction{Handler: storagePoolBucketKeysPost, AccessHandler: allowPermission(entity.TypeStorageBucket, auth.EntitlementCanEdit, "poolName", "bucketName")}, + Get: APIEndpointAction{Handler: storagePoolBucketKeysGet, AccessHandler: storageBucketAccessHandler(auth.EntitlementCanView)}, + Post: APIEndpointAction{Handler: storagePoolBucketKeysPost, AccessHandler: storageBucketAccessHandler(auth.EntitlementCanEdit)}, } var storagePoolBucketKeyCmd = APIEndpoint{ Path: "storage-pools/{poolName}/buckets/{bucketName}/keys/{keyName}", - Delete: APIEndpointAction{Handler: storagePoolBucketKeyDelete, AccessHandler: allowPermission(entity.TypeStorageBucket, auth.EntitlementCanEdit, "poolName", "bucketName")}, - Get: APIEndpointAction{Handler: storagePoolBucketKeyGet, AccessHandler: allowPermission(entity.TypeStorageBucket, auth.EntitlementCanView, "poolName", "bucketName")}, - Put: APIEndpointAction{Handler: storagePoolBucketKeyPut, AccessHandler: allowPermission(entity.TypeStorageBucket, auth.EntitlementCanEdit, "poolName", "bucketName")}, + Delete: APIEndpointAction{Handler: storagePoolBucketKeyDelete, AccessHandler: storageBucketAccessHandler(auth.EntitlementCanEdit)}, + Get: APIEndpointAction{Handler: storagePoolBucketKeyGet, AccessHandler: storageBucketAccessHandler(auth.EntitlementCanView)}, + Put: APIEndpointAction{Handler: storagePoolBucketKeyPut, AccessHandler: storageBucketAccessHandler(auth.EntitlementCanEdit)}, +} + +// storageBucketAccessHandler returns an access handler that checks for the given entitlement against a storage bucket. +// The storage pool containing the bucket and the effective project of the bucket are added to the request context for +// later use. +func storageBucketAccessHandler(entitlement auth.Entitlement) func(d *Daemon, r *http.Request) response.Response { + return func(d *Daemon, r *http.Request) response.Response { + s := d.State() + + err := addStorageBucketDetailsToContext(d, r) + if err != nil { + return response.SmartError(err) + } + + details, err := request.GetCtxValue[storageBucketDetails](r.Context(), ctxStorageBucketDetails) + if err != nil { + return nil + } + + // If the storage pool is a remote driver, the auth subsystem does not require a target parameter to create a + // unique URL for the storage bucket. So even if the caller supplied a target parameter, we don't use it in the + // access check if the pool is remote. + target := "" + if !details.pool.Driver().Info().Remote { + target = request.QueryParam(r, "target") + } + + err = s.Authorizer.CheckPermission(r.Context(), entity.StorageBucketURL(request.ProjectParam(r), target, details.pool.Name(), details.bucketName), entitlement) + if err != nil { + return response.SmartError(err) + } + + return response.EmptySyncResponse + } } // API endpoints @@ -204,7 +238,7 @@ func storagePoolBucketsGet(d *Daemon, r *http.Request) response.Response { var filteredDBBuckets []*db.StorageBucket for _, bucket := range dbBuckets { - if !userHasPermission(entity.StorageBucketURL(requestProjectName, "", poolName, bucket.Name)) { + if !userHasPermission(entity.StorageBucketURL(requestProjectName, bucket.Location, poolName, bucket.Name)) { continue } @@ -289,43 +323,33 @@ func storagePoolBucketGet(d *Daemon, r *http.Request) response.Response { return resp } - bucketProjectName, err := project.StorageBucketProject(r.Context(), s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) + details, err := request.GetCtxValue[storageBucketDetails](r.Context(), ctxStorageBucketDetails) if err != nil { return response.SmartError(err) } - pool, err := storagePools.LoadByName(s, poolName) - if err != nil { - return response.SmartError(fmt.Errorf("Failed loading storage pool: %w", err)) - } - - if !pool.Driver().Info().Buckets { + if !details.pool.Driver().Info().Buckets { return response.BadRequest(fmt.Errorf("Storage pool does not support buckets")) } - bucketName, err := url.PathUnescape(mux.Vars(r)["bucketName"]) - if err != nil { - return response.SmartError(err) - } - targetMember := request.QueryParam(r, "target") memberSpecific := targetMember != "" var bucket *db.StorageBucket err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - bucket, err = tx.GetStoragePoolBucket(ctx, pool.ID(), bucketProjectName, memberSpecific, bucketName) + bucket, err = tx.GetStoragePoolBucket(ctx, details.pool.ID(), effectiveProjectName, memberSpecific, details.bucketName) return err }) if err != nil { return response.SmartError(err) } - u := pool.GetBucketURL(bucket.Name) + u := details.pool.GetBucketURL(bucket.Name) if u != nil { bucket.S3URL = u.String() } @@ -514,22 +538,12 @@ func storagePoolBucketPut(d *Daemon, r *http.Request) response.Response { return resp } - bucketProjectName, err := project.StorageBucketProject(r.Context(), s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - - pool, err := storagePools.LoadByName(s, poolName) - if err != nil { - return response.SmartError(fmt.Errorf("Failed loading storage pool: %w", err)) - } - - bucketName, err := url.PathUnescape(mux.Vars(r)["bucketName"]) + details, err := request.GetCtxValue[storageBucketDetails](r.Context(), ctxStorageBucketDetails) if err != nil { return response.SmartError(err) } @@ -547,7 +561,7 @@ func storagePoolBucketPut(d *Daemon, r *http.Request) response.Response { var bucket *db.StorageBucket err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - bucket, err = tx.GetStoragePoolBucket(ctx, pool.ID(), bucketProjectName, memberSpecific, bucketName) + bucket, err = tx.GetStoragePoolBucket(ctx, details.pool.ID(), effectiveProjectName, memberSpecific, details.bucketName) return err }) if err != nil { @@ -564,12 +578,12 @@ func storagePoolBucketPut(d *Daemon, r *http.Request) response.Response { } } - err = pool.UpdateBucket(bucketProjectName, bucketName, req, nil) + err = details.pool.UpdateBucket(effectiveProjectName, details.bucketName, req, nil) if err != nil { return response.SmartError(fmt.Errorf("Failed updating storage bucket: %w", err)) } - s.Events.SendLifecycle(bucketProjectName, lifecycle.StorageBucketUpdated.Event(pool, bucketProjectName, bucketName, request.CreateRequestor(r), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.StorageBucketUpdated.Event(details.pool, effectiveProjectName, details.bucketName, request.CreateRequestor(r), nil)) return response.EmptySyncResponse } @@ -611,32 +625,22 @@ func storagePoolBucketDelete(d *Daemon, r *http.Request) response.Response { return resp } - bucketProjectName, err := project.StorageBucketProject(r.Context(), s.DB.Cluster, request.ProjectParam(r)) - if err != nil { - return response.SmartError(err) - } - - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - pool, err := storagePools.LoadByName(s, poolName) - if err != nil { - return response.SmartError(fmt.Errorf("Failed loading storage pool: %w", err)) - } - - bucketName, err := url.PathUnescape(mux.Vars(r)["bucketName"]) + details, err := request.GetCtxValue[storageBucketDetails](r.Context(), ctxStorageBucketDetails) if err != nil { return response.SmartError(err) } - err = pool.DeleteBucket(bucketProjectName, bucketName, nil) + err = details.pool.DeleteBucket(effectiveProjectName, details.bucketName, nil) if err != nil { return response.SmartError(fmt.Errorf("Failed deleting storage bucket: %w", err)) } - s.Events.SendLifecycle(bucketProjectName, lifecycle.StorageBucketDeleted.Event(pool, bucketProjectName, bucketName, request.CreateRequestor(r), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.StorageBucketDeleted.Event(details.pool, effectiveProjectName, details.bucketName, request.CreateRequestor(r), nil)) return response.EmptySyncResponse } @@ -743,31 +747,21 @@ func storagePoolBucketKeysGet(d *Daemon, r *http.Request) response.Response { return resp } - bucketProjectName, err := project.StorageBucketProject(r.Context(), s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) + details, err := request.GetCtxValue[storageBucketDetails](r.Context(), ctxStorageBucketDetails) if err != nil { return response.SmartError(err) } - pool, err := storagePools.LoadByName(s, poolName) - if err != nil { - return response.SmartError(fmt.Errorf("Failed loading storage pool: %w", err)) - } - - driverInfo := pool.Driver().Info() + driverInfo := details.pool.Driver().Info() if !driverInfo.Buckets { return response.BadRequest(fmt.Errorf("Storage pool driver %q does not support buckets", driverInfo.Name)) } - bucketName, err := url.PathUnescape(mux.Vars(r)["bucketName"]) - if err != nil { - return response.SmartError(err) - } - // If target is set, get buckets only for this cluster members. targetMember := request.QueryParam(r, "target") memberSpecific := targetMember != "" @@ -775,7 +769,7 @@ func storagePoolBucketKeysGet(d *Daemon, r *http.Request) response.Response { var dbBucket *db.StorageBucket var dbBucketKeys []*db.StorageBucketKey err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - dbBucket, err = tx.GetStoragePoolBucket(ctx, pool.ID(), bucketProjectName, memberSpecific, bucketName) + dbBucket, err = tx.GetStoragePoolBucket(ctx, details.pool.ID(), effectiveProjectName, memberSpecific, details.bucketName) if err != nil { return fmt.Errorf("Failed loading storage bucket: %w", err) } @@ -802,7 +796,7 @@ func storagePoolBucketKeysGet(d *Daemon, r *http.Request) response.Response { bucketKeyURLs := make([]string, 0, len(dbBucketKeys)) for _, dbBucketKey := range dbBucketKeys { - bucketKeyURLs = append(bucketKeyURLs, dbBucketKey.URL(version.APIVersion, poolName, bucketProjectName, bucketName).String()) + bucketKeyURLs = append(bucketKeyURLs, dbBucketKey.URL(version.APIVersion, details.pool.Name(), effectiveProjectName, details.bucketName).String()) } return response.SyncResponse(true, bucketKeyURLs) @@ -848,17 +842,12 @@ func storagePoolBucketKeysPost(d *Daemon, r *http.Request) response.Response { return resp } - bucketProjectName, err := project.StorageBucketProject(r.Context(), s.DB.Cluster, request.ProjectParam(r)) - if err != nil { - return response.SmartError(err) - } - - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - bucketName, err := url.PathUnescape(mux.Vars(r)["bucketName"]) + details, err := request.GetCtxValue[storageBucketDetails](r.Context(), ctxStorageBucketDetails) if err != nil { return response.SmartError(err) } @@ -870,18 +859,13 @@ func storagePoolBucketKeysPost(d *Daemon, r *http.Request) response.Response { return response.BadRequest(err) } - pool, err := storagePools.LoadByName(s, poolName) - if err != nil { - return response.SmartError(fmt.Errorf("Failed loading storage pool: %w", err)) - } - - key, err := pool.CreateBucketKey(bucketProjectName, bucketName, req, nil) + key, err := details.pool.CreateBucketKey(effectiveProjectName, details.bucketName, req, nil) if err != nil { return response.SmartError(fmt.Errorf("Failed creating storage bucket key: %w", err)) } - lc := lifecycle.StorageBucketKeyCreated.Event(pool, bucketProjectName, pool.Name(), req.Name, request.CreateRequestor(r), nil) - s.Events.SendLifecycle(bucketProjectName, lc) + lc := lifecycle.StorageBucketKeyCreated.Event(details.pool, effectiveProjectName, details.pool.Name(), req.Name, request.CreateRequestor(r), nil) + s.Events.SendLifecycle(effectiveProjectName, lc) return response.SyncResponseLocation(true, key, lc.Source) } @@ -923,22 +907,12 @@ func storagePoolBucketKeyDelete(d *Daemon, r *http.Request) response.Response { return resp } - bucketProjectName, err := project.StorageBucketProject(r.Context(), s.DB.Cluster, request.ProjectParam(r)) - if err != nil { - return response.SmartError(err) - } - - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - pool, err := storagePools.LoadByName(s, poolName) - if err != nil { - return response.SmartError(fmt.Errorf("Failed loading storage pool: %w", err)) - } - - bucketName, err := url.PathUnescape(mux.Vars(r)["bucketName"]) + details, err := request.GetCtxValue[storageBucketDetails](r.Context(), ctxStorageBucketDetails) if err != nil { return response.SmartError(err) } @@ -948,12 +922,12 @@ func storagePoolBucketKeyDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - err = pool.DeleteBucketKey(bucketProjectName, bucketName, keyName, nil) + err = details.pool.DeleteBucketKey(effectiveProjectName, details.bucketName, keyName, nil) if err != nil { return response.SmartError(fmt.Errorf("Failed deleting storage bucket key: %w", err)) } - s.Events.SendLifecycle(bucketProjectName, lifecycle.StorageBucketKeyDeleted.Event(pool, bucketProjectName, pool.Name(), bucketName, request.CreateRequestor(r), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.StorageBucketKeyDeleted.Event(details.pool, effectiveProjectName, details.pool.Name(), details.bucketName, request.CreateRequestor(r), nil)) return response.EmptySyncResponse } @@ -1006,30 +980,20 @@ func storagePoolBucketKeyGet(d *Daemon, r *http.Request) response.Response { return resp } - bucketProjectName, err := project.StorageBucketProject(r.Context(), s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) + details, err := request.GetCtxValue[storageBucketDetails](r.Context(), ctxStorageBucketDetails) if err != nil { return response.SmartError(err) } - pool, err := storagePools.LoadByName(s, poolName) - if err != nil { - return response.SmartError(fmt.Errorf("Failed loading storage pool: %w", err)) - } - - if !pool.Driver().Info().Buckets { + if !details.pool.Driver().Info().Buckets { return response.BadRequest(fmt.Errorf("Storage pool does not support buckets")) } - bucketName, err := url.PathUnescape(mux.Vars(r)["bucketName"]) - if err != nil { - return response.SmartError(err) - } - keyName, err := url.PathUnescape(mux.Vars(r)["keyName"]) if err != nil { return response.SmartError(err) @@ -1040,7 +1004,7 @@ func storagePoolBucketKeyGet(d *Daemon, r *http.Request) response.Response { var bucketKey *db.StorageBucketKey err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - bucket, err := tx.GetStoragePoolBucket(ctx, pool.ID(), bucketProjectName, memberSpecific, bucketName) + bucket, err := tx.GetStoragePoolBucket(ctx, details.pool.ID(), effectiveProjectName, memberSpecific, details.bucketName) if err != nil { return err } @@ -1106,22 +1070,12 @@ func storagePoolBucketKeyPut(d *Daemon, r *http.Request) response.Response { return resp } - bucketProjectName, err := project.StorageBucketProject(r.Context(), s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - - pool, err := storagePools.LoadByName(s, poolName) - if err != nil { - return response.SmartError(fmt.Errorf("Failed loading storage pool: %w", err)) - } - - bucketName, err := url.PathUnescape(mux.Vars(r)["bucketName"]) + details, err := request.GetCtxValue[storageBucketDetails](r.Context(), ctxStorageBucketDetails) if err != nil { return response.SmartError(err) } @@ -1138,12 +1092,66 @@ func storagePoolBucketKeyPut(d *Daemon, r *http.Request) response.Response { return response.BadRequest(err) } - err = pool.UpdateBucketKey(bucketProjectName, bucketName, keyName, req, nil) + err = details.pool.UpdateBucketKey(effectiveProjectName, details.bucketName, keyName, req, nil) if err != nil { return response.SmartError(fmt.Errorf("Failed updating storage bucket key: %w", err)) } - s.Events.SendLifecycle(bucketProjectName, lifecycle.StorageBucketKeyUpdated.Event(pool, bucketProjectName, pool.Name(), bucketName, request.CreateRequestor(r), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.StorageBucketKeyUpdated.Event(details.pool, effectiveProjectName, details.pool.Name(), details.bucketName, request.CreateRequestor(r), nil)) return response.EmptySyncResponse } + +// ctxStorageBucketDetails is the request.CtxKey corresponding to storageBucketDetails, which is added to the request +// context in addStorageBucketDetailsToContext. +const ctxStorageBucketDetails request.CtxKey = "storage-bucket-details" + +// storageBucketDetails contains details common to all storage volume requests. A value of this type is added to the +// request context when addStorageBucketDetailsToContext is called. We do this to avoid repeated logic when +// parsing the request details and/or making database calls to get the storage pool or effective project. These fields +// are required for the storage bucket access check, and are subsequently available in the storage bucket handlers. +type storageBucketDetails struct { + bucketName string + pool storagePools.Pool +} + +// addStorageBucketDetailsToContext extracts storageBucketDetails from the http.Request and adds it to the +// request context with the ctxStorageBucketDetails request.CtxKey. Additionally, the effective project of the storage +// bucket is added to the request context under request.CtxEffectiveProjectName. +func addStorageBucketDetailsToContext(d *Daemon, r *http.Request) error { + var details storageBucketDetails + defer func() { + request.SetCtxValue(r, ctxStorageBucketDetails, details) + }() + + s := d.State() + + projectName := request.ProjectParam(r) + + effectiveProjectName, err := project.StorageBucketProject(r.Context(), s.DB.Cluster, projectName) + if err != nil { + return err + } + + request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProjectName) + + poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) + if err != nil { + return err + } + + pool, err := storagePools.LoadByName(s, poolName) + if err != nil { + return fmt.Errorf("Failed loading storage pool: %w", err) + } + + details.pool = pool + + bucketName, err := url.PathUnescape(mux.Vars(r)["bucketName"]) + if err != nil { + return err + } + + details.bucketName = bucketName + return nil +} diff --git a/lxd/storage_volumes.go b/lxd/storage_volumes.go index 57cfd91705a2..4f947bbd0ee0 100644 --- a/lxd/storage_volumes.go +++ b/lxd/storage_volumes.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "encoding/json" "encoding/pem" + "errors" "fmt" "io" "net/http" @@ -73,11 +74,58 @@ var storagePoolVolumesTypeCmd = APIEndpoint{ var storagePoolVolumeTypeCmd = APIEndpoint{ Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}", - Delete: APIEndpointAction{Handler: storagePoolVolumeDelete, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanDelete, "poolName", "type", "volumeName")}, - Get: APIEndpointAction{Handler: storagePoolVolumeGet, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanView, "poolName", "type", "volumeName")}, - Patch: APIEndpointAction{Handler: storagePoolVolumePatch, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanEdit, "poolName", "type", "volumeName")}, - Post: APIEndpointAction{Handler: storagePoolVolumePost, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanEdit, "poolName", "type", "volumeName")}, - Put: APIEndpointAction{Handler: storagePoolVolumePut, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanEdit, "poolName", "type", "volumeName")}, + Delete: APIEndpointAction{Handler: storagePoolVolumeDelete, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanDelete)}, + Get: APIEndpointAction{Handler: storagePoolVolumeGet, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanView)}, + Patch: APIEndpointAction{Handler: storagePoolVolumePatch, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanEdit)}, + Post: APIEndpointAction{Handler: storagePoolVolumePost, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanEdit)}, + Put: APIEndpointAction{Handler: storagePoolVolumePut, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanEdit)}, +} + +// storagePoolVolumeTypeAccessHandler returns an access handler which checks the given entitlement on a storage volume. +func storagePoolVolumeTypeAccessHandler(entitlement auth.Entitlement) func(d *Daemon, r *http.Request) response.Response { + return func(d *Daemon, r *http.Request) response.Response { + s := d.State() + err := addStoragePoolVolumeDetailsToRequestContext(s, r) + if err != nil { + return response.SmartError(err) + } + + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) + if err != nil { + return response.SmartError(err) + } + + var target string + + // Regardless of whether the caller specified a target parameter, we do not add it to the authorization check if + // the storage pool is remote. This is because the volume in the database has a NULL `node_id`, so the URL uniquely + // identifies the volume without the target parameter. + if !details.pool.Driver().Info().Remote { + // If the storage pool is local, we need to add a target parameter to the authorization check URL for the + // auth subsystem to consider it unique. + + // If the target parameter was specified, use that. + target = request.QueryParam(r, "target") + + if target == "" { + // Otherwise, check if the volume is located on another member. + if details.forwardingNodeInfo != nil { + // Use the name of the forwarding member as the location of the volume. + target = details.forwardingNodeInfo.Name + } else { + // If we're not forwarding the request, use the name of this member as the location of the volume. + target = s.ServerName + } + } + } + + err = s.Authorizer.CheckPermission(r.Context(), entity.StorageVolumeURL(request.ProjectParam(r), target, details.pool.Name(), details.volumeTypeName, details.volumeName), entitlement) + if err != nil { + return response.SmartError(err) + } + + return response.EmptySyncResponse + } } // swagger:operation GET /1.0/storage-volumes storage storage_volumes_get @@ -756,7 +804,7 @@ func storagePoolVolumesGet(d *Daemon, r *http.Request) response.Response { vol := &dbVol.StorageVolume volumeName, _, _ := api.GetParentAndSnapshotName(vol.Name) - if !userHasPermission(entity.StorageVolumeURL(vol.Project, "", dbVol.Pool, dbVol.Type, volumeName)) { + if !userHasPermission(entity.StorageVolumeURL(vol.Project, vol.Location, dbVol.Pool, dbVol.Type, volumeName)) { continue } @@ -780,7 +828,7 @@ func storagePoolVolumesGet(d *Daemon, r *http.Request) response.Response { for _, dbVol := range dbVolumes { volumeName, _, _ := api.GetParentAndSnapshotName(dbVol.Name) - if !userHasPermission(entity.StorageVolumeURL(dbVol.Project, "", dbVol.Pool, dbVol.Type, volumeName)) { + if !userHasPermission(entity.StorageVolumeURL(dbVol.Project, dbVol.Location, dbVol.Pool, dbVol.Type, volumeName)) { continue } @@ -1313,27 +1361,15 @@ func doVolumeMigration(s *state.State, r *http.Request, requestProjectName strin func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { s := d.State() - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) - if err != nil { - return response.SmartError(err) - } - - if shared.IsSnapshot(volumeName) { + if shared.IsSnapshot(details.volumeName) { return response.BadRequest(fmt.Errorf("Invalid volume name")) } - // Get the name of the storage pool the volume is supposed to be attached to. - srcPoolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - req := api.StorageVolumePost{} // Parse the request. @@ -1350,17 +1386,17 @@ func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { // We currently only allow to create storage volumes of type storagePoolVolumeTypeCustom. // So check, that nothing else was requested. - if volumeTypeName != cluster.StoragePoolVolumeTypeNameCustom { - return response.BadRequest(fmt.Errorf("Renaming storage volumes of type %q is not allowed", volumeTypeName)) + if details.volumeTypeName != cluster.StoragePoolVolumeTypeNameCustom { + return response.BadRequest(fmt.Errorf("Renaming storage volumes of type %q is not allowed", details.volumeTypeName)) } requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, cluster.StoragePoolVolumeTypeCustom) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - targetProjectName := projectName + targetProjectName := effectiveProjectName if req.Project != "" { targetProjectName, err = project.StorageVolumeProject(s.DB.Cluster, req.Project, cluster.StoragePoolVolumeTypeCustom) if err != nil { @@ -1375,11 +1411,11 @@ func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { return response.BadRequest(fmt.Errorf("Target project does not have features.storage.volumes enabled")) } - if projectName == targetProjectName { + if effectiveProjectName == targetProjectName { return response.BadRequest(fmt.Errorf("Project and target project are the same")) } - // Check if user has access to effective storage target project + // Check if user has permission to copy/move the volume into the effective project corresponding to the target. err := s.Authorizer.CheckPermission(r.Context(), entity.ProjectURL(targetProjectName), auth.EntitlementCanCreateStorageVolumes) if err != nil { return response.SmartError(err) @@ -1439,24 +1475,13 @@ func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { return resp } - srcPool, err := storagePools.LoadByName(s, srcPoolName) - if err != nil { - return response.SmartError(err) - } - - if srcPool.Driver().Info().Name == "ceph" { + if details.pool.Driver().Info().Name == "ceph" { var dbVolume *db.StorageVolume var volumeNotFound bool var targetIsSet bool err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - // Load source volume. - srcPoolID, err := tx.GetStoragePoolID(ctx, srcPoolName) - if err != nil { - return err - } - - dbVolume, err = tx.GetStoragePoolVolume(ctx, srcPoolID, projectName, cluster.StoragePoolVolumeTypeCustom, volumeName, true) + dbVolume, err = tx.GetStoragePoolVolume(ctx, details.pool.ID(), effectiveProjectName, cluster.StoragePoolVolumeTypeCustom, details.volumeName, true) if err != nil { // Check if the user provided an incorrect target query parameter and return a helpful error message. _, volumeNotFound = api.StatusErrorMatch(err, http.StatusNotFound) @@ -1479,7 +1504,7 @@ func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { Name: req.Name, } - return storagePoolVolumeTypePostRename(s, r, srcPool.Name(), projectName, &dbVolume.StorageVolume, req) + return storagePoolVolumeTypePostRename(s, r, details.pool.Name(), effectiveProjectName, &dbVolume.StorageVolume, req) } } else { resp := forwardedResponseToNode(s, r, req.Source.Location) @@ -1489,7 +1514,7 @@ func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { } err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - p, err := cluster.GetProject(ctx, tx.Tx(), projectName) + p, err := cluster.GetProject(ctx, tx.Tx(), effectiveProjectName) if err != nil { return err } @@ -1524,13 +1549,13 @@ func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { } run := func(op *operations.Operation) error { - return migrateStorageVolume(s, r, volumeName, srcPoolName, targetMemberInfo.Name, targetProjectName, req, op) + return migrateStorageVolume(s, r, details.volumeName, details.pool.Name(), targetMemberInfo.Name, targetProjectName, req, op) } resources := map[string][]api.URL{} - resources["storage_volumes"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", srcPoolName, "volumes", "custom", volumeName)} + resources["storage_volumes"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", details.pool.Name(), "volumes", "custom", details.volumeName)} - op, err := operations.OperationCreate(s, projectName, operations.OperationClassTask, operationtype.VolumeMigrate, resources, nil, run, nil, nil, r) + op, err := operations.OperationCreate(s, effectiveProjectName, operations.OperationClassTask, operationtype.VolumeMigrate, resources, nil, run, nil, nil, r) if err != nil { return response.InternalError(err) } @@ -1543,15 +1568,9 @@ func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { return resp } - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - // If source is set, we know the source and the target, and therefore don't need this function to figure out where to forward the request to. if req.Source.Location == "" { - resp = forwardedResponseIfVolumeIsRemote(s, r, srcPoolName, projectName, volumeName, volumeType) + resp := forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } @@ -1559,7 +1578,7 @@ func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { // This is a migration request so send back requested secrets. if req.Migration { - return storagePoolVolumeTypePostMigration(s, r, requestProjectName, projectName, srcPoolName, volumeName, req) + return storagePoolVolumeTypePostMigration(s, r, requestProjectName, effectiveProjectName, details.pool.Name(), details.volumeName, req) } // Retrieve ID of the storage pool (and check if the storage pool exists). @@ -1569,7 +1588,7 @@ func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { if req.Pool != "" { targetPoolName = req.Pool } else { - targetPoolName = srcPoolName + targetPoolName = details.pool.Name() } err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { @@ -1583,7 +1602,7 @@ func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { // Check that the name isn't already in use. - _, err = tx.GetStoragePoolNodeVolumeID(ctx, targetProjectName, req.Name, volumeType, targetPoolID) + _, err = tx.GetStoragePoolNodeVolumeID(ctx, targetProjectName, req.Name, details.volumeType, targetPoolID) return err }) @@ -1596,7 +1615,7 @@ func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { } // Check if the daemon itself is using it. - used, err := storagePools.VolumeUsedByDaemon(s, srcPoolName, volumeName) + used, err := storagePools.VolumeUsedByDaemon(s, details.pool.Name(), details.volumeName) if err != nil { return response.SmartError(err) } @@ -1610,13 +1629,7 @@ func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { var targetIsSet bool err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - // Load source volume. - srcPoolID, err := tx.GetStoragePoolID(ctx, srcPoolName) - if err != nil { - return err - } - - dbVolume, err = tx.GetStoragePoolVolume(ctx, srcPoolID, projectName, volumeType, volumeName, true) + dbVolume, err = tx.GetStoragePoolVolume(ctx, details.pool.ID(), effectiveProjectName, details.volumeType, details.volumeName, true) if err != nil { // Check if the user provided an incorrect target query parameter and return a helpful error message. _, volumeNotFound = api.StatusErrorMatch(err, http.StatusNotFound) @@ -1636,7 +1649,7 @@ func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { } // Check if a running instance is using it. - err = storagePools.VolumeUsedByInstanceDevices(s, srcPoolName, projectName, &dbVolume.StorageVolume, true, func(dbInst db.InstanceArgs, project api.Project, usedByDevices []string) error { + err = storagePools.VolumeUsedByInstanceDevices(s, details.pool.Name(), effectiveProjectName, &dbVolume.StorageVolume, true, func(dbInst db.InstanceArgs, project api.Project, usedByDevices []string) error { inst, err := instance.Load(s, dbInst, project) if err != nil { return err @@ -1653,12 +1666,12 @@ func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { } // Detect a rename request. - if (req.Pool == "" || req.Pool == srcPoolName) && (projectName == targetProjectName) { - return storagePoolVolumeTypePostRename(s, r, srcPoolName, projectName, &dbVolume.StorageVolume, req) + if (req.Pool == "" || req.Pool == details.pool.Name()) && (effectiveProjectName == targetProjectName) { + return storagePoolVolumeTypePostRename(s, r, details.pool.Name(), effectiveProjectName, &dbVolume.StorageVolume, req) } // Otherwise this is a move request. - return storagePoolVolumeTypePostMove(s, r, srcPoolName, projectName, targetProjectName, &dbVolume.StorageVolume, req) + return storagePoolVolumeTypePostMove(s, r, details.pool.Name(), effectiveProjectName, targetProjectName, &dbVolume.StorageVolume, req) } func migrateStorageVolume(s *state.State, r *http.Request, sourceVolumeName string, sourcePoolName string, targetNode string, projectName string, req api.StorageVolumePost, op *operations.Operation) error { @@ -1968,36 +1981,18 @@ func storagePoolVolumeTypePostMove(s *state.State, r *http.Request, poolName str func storagePoolVolumeGet(d *Daemon, r *http.Request) response.Response { s := d.State() - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the storage pool the volume is supposed to be attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - // Check that the storage volume type is valid. - if !shared.ValueInSlice(volumeType, supportedVolumeTypes) { - return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName)) + if !shared.ValueInSlice(details.volumeType, supportedVolumeTypes) { + return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", details.volumeTypeName)) } requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, volumeType) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } @@ -2007,7 +2002,7 @@ func storagePoolVolumeGet(d *Daemon, r *http.Request) response.Response { return resp } - resp = forwardedResponseIfVolumeIsRemote(s, r, poolName, projectName, volumeName, volumeType) + resp = forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } @@ -2015,14 +2010,8 @@ func storagePoolVolumeGet(d *Daemon, r *http.Request) response.Response { var dbVolume *db.StorageVolume err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - // Get the ID of the storage pool the storage volume is supposed to be attached to. - poolID, err := tx.GetStoragePoolID(ctx, poolName) - if err != nil { - return err - } - // Get the storage volume. - dbVolume, err = tx.GetStoragePoolVolume(ctx, poolID, projectName, volumeType, volumeName, true) + dbVolume, err = tx.GetStoragePoolVolume(ctx, details.pool.ID(), effectiveProjectName, details.volumeType, details.volumeName, true) return err }) if err != nil { @@ -2036,7 +2025,7 @@ func storagePoolVolumeGet(d *Daemon, r *http.Request) response.Response { dbVolume.UsedBy = project.FilterUsedBy(s.Authorizer, r, volumeUsedBy) - etag := []any{volumeName, dbVolume.Type, dbVolume.Config} + etag := []any{details.volumeName, dbVolume.Type, dbVolume.Config} return response.SyncResponseETag(true, dbVolume.StorageVolume, etag) } @@ -2083,43 +2072,19 @@ func storagePoolVolumeGet(d *Daemon, r *http.Request) response.Response { func storagePoolVolumePut(d *Daemon, r *http.Request) response.Response { s := d.State() - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the storage pool the volume is supposed to be attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - - requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, volumeType) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } // Check that the storage volume type is valid. - if !shared.ValueInSlice(volumeType, supportedVolumeTypes) { - return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName)) - } - - pool, err := storagePools.LoadByName(s, poolName) - if err != nil { - return response.SmartError(err) + if !shared.ValueInSlice(details.volumeType, supportedVolumeTypes) { + return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", details.volumeTypeName)) } resp := forwardedResponseIfTargetIsRemote(s, r) @@ -2127,7 +2092,7 @@ func storagePoolVolumePut(d *Daemon, r *http.Request) response.Response { return resp } - resp = forwardedResponseIfVolumeIsRemote(s, r, pool.Name(), projectName, volumeName, volumeType) + resp = forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } @@ -2135,7 +2100,7 @@ func storagePoolVolumePut(d *Daemon, r *http.Request) response.Response { // Get the existing storage volume. var dbVolume *db.StorageVolume err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - dbVolume, err = tx.GetStoragePoolVolume(ctx, pool.ID(), projectName, volumeType, volumeName, true) + dbVolume, err = tx.GetStoragePoolVolume(ctx, details.pool.ID(), effectiveProjectName, details.volumeType, details.volumeName, true) return err }) if err != nil { @@ -2143,7 +2108,7 @@ func storagePoolVolumePut(d *Daemon, r *http.Request) response.Response { } // Validate the ETag - etag := []any{volumeName, dbVolume.Type, dbVolume.Config} + etag := []any{details.volumeName, dbVolume.Type, dbVolume.Config} err = util.EtagCheck(r, etag) if err != nil { @@ -2160,12 +2125,12 @@ func storagePoolVolumePut(d *Daemon, r *http.Request) response.Response { op := &operations.Operation{} op.SetRequestor(r) - if volumeType == cluster.StoragePoolVolumeTypeCustom { + if details.volumeType == cluster.StoragePoolVolumeTypeCustom { // Restore custom volume from snapshot if requested. This should occur first // before applying config changes so that changes are applied to the // restored volume. if req.Restore != "" { - err = pool.RestoreCustomVolume(projectName, dbVolume.Name, req.Restore, op) + err = details.pool.RestoreCustomVolume(effectiveProjectName, dbVolume.Name, req.Restore, op) if err != nil { return response.SmartError(err) } @@ -2177,31 +2142,31 @@ func storagePoolVolumePut(d *Daemon, r *http.Request) response.Response { if req.Config != nil || req.Restore == "" { // Possibly check if project limits are honored. err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - return project.AllowVolumeUpdate(s.GlobalConfig, tx, projectName, volumeName, req, dbVolume.Config) + return project.AllowVolumeUpdate(s.GlobalConfig, tx, effectiveProjectName, details.volumeName, req, dbVolume.Config) }) if err != nil { return response.SmartError(err) } - err = pool.UpdateCustomVolume(projectName, dbVolume.Name, req.Description, req.Config, op) + err = details.pool.UpdateCustomVolume(effectiveProjectName, dbVolume.Name, req.Description, req.Config, op) if err != nil { return response.SmartError(err) } } - } else if volumeType == cluster.StoragePoolVolumeTypeContainer || volumeType == cluster.StoragePoolVolumeTypeVM { - inst, err := instance.LoadByProjectAndName(s, projectName, dbVolume.Name) + } else if details.volumeType == cluster.StoragePoolVolumeTypeContainer || details.volumeType == cluster.StoragePoolVolumeTypeVM { + inst, err := instance.LoadByProjectAndName(s, effectiveProjectName, dbVolume.Name) if err != nil { return response.SmartError(err) } // Handle instance volume update requests. - err = pool.UpdateInstance(inst, req.Description, req.Config, op) + err = details.pool.UpdateInstance(inst, req.Description, req.Config, op) if err != nil { return response.SmartError(err) } - } else if volumeType == cluster.StoragePoolVolumeTypeImage { + } else if details.volumeType == cluster.StoragePoolVolumeTypeImage { // Handle image update requests. - err = pool.UpdateImage(dbVolume.Name, req.Description, req.Config, op) + err = details.pool.UpdateImage(dbVolume.Name, req.Description, req.Config, op) if err != nil { return response.SmartError(err) } @@ -2254,45 +2219,21 @@ func storagePoolVolumePut(d *Daemon, r *http.Request) response.Response { func storagePoolVolumePatch(d *Daemon, r *http.Request) response.Response { s := d.State() - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) - if err != nil { - return response.SmartError(err) - } - - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } - if shared.IsSnapshot(volumeName) { + if shared.IsSnapshot(details.volumeName) { return response.BadRequest(fmt.Errorf("Invalid volume name")) } - // Get the name of the storage pool the volume is supposed to be attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - // Check that the storage volume type is custom. - if volumeType != cluster.StoragePoolVolumeTypeCustom { - return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName)) - } - - requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, volumeType) - if err != nil { - return response.SmartError(err) + if details.volumeType != cluster.StoragePoolVolumeTypeCustom { + return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", details.volumeTypeName)) } - pool, err := storagePools.LoadByName(s, poolName) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } @@ -2302,7 +2243,7 @@ func storagePoolVolumePatch(d *Daemon, r *http.Request) response.Response { return resp } - resp = forwardedResponseIfVolumeIsRemote(s, r, pool.Name(), projectName, volumeName, volumeType) + resp = forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } @@ -2310,7 +2251,7 @@ func storagePoolVolumePatch(d *Daemon, r *http.Request) response.Response { // Get the existing storage volume. var dbVolume *db.StorageVolume err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - dbVolume, err = tx.GetStoragePoolVolume(ctx, pool.ID(), projectName, volumeType, volumeName, true) + dbVolume, err = tx.GetStoragePoolVolume(ctx, details.pool.ID(), effectiveProjectName, details.volumeType, details.volumeName, true) return err }) if err != nil { @@ -2318,7 +2259,7 @@ func storagePoolVolumePatch(d *Daemon, r *http.Request) response.Response { } // Validate the ETag. - etag := []any{volumeName, dbVolume.Type, dbVolume.Config} + etag := []any{details.volumeName, dbVolume.Type, dbVolume.Config} err = util.EtagCheck(r, etag) if err != nil { @@ -2347,7 +2288,7 @@ func storagePoolVolumePatch(d *Daemon, r *http.Request) response.Response { op := &operations.Operation{} op.SetRequestor(r) - err = pool.UpdateCustomVolume(projectName, dbVolume.Name, req.Description, req.Config, op) + err = details.pool.UpdateCustomVolume(effectiveProjectName, dbVolume.Name, req.Description, req.Config, op) if err != nil { return response.SmartError(err) } @@ -2387,42 +2328,18 @@ func storagePoolVolumePatch(d *Daemon, r *http.Request) response.Response { func storagePoolVolumeDelete(d *Daemon, r *http.Request) response.Response { s := d.State() - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) - if err != nil { - return response.SmartError(err) - } - - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } - if shared.IsSnapshot(volumeName) { - return response.BadRequest(fmt.Errorf("Invalid storage volume %q", volumeName)) - } - - // Get the name of the storage pool the volume is supposed to be attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - - requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, volumeType) - if err != nil { - return response.SmartError(err) + if shared.IsSnapshot(details.volumeName) { + return response.BadRequest(fmt.Errorf("Invalid storage volume %q", details.volumeName)) } // Check that the storage volume type is valid. - if !shared.ValueInSlice(volumeType, supportedVolumeTypes) { - return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName)) + if !shared.ValueInSlice(details.volumeType, supportedVolumeTypes) { + return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", details.volumeTypeName)) } resp := forwardedResponseIfTargetIsRemote(s, r) @@ -2430,25 +2347,25 @@ func storagePoolVolumeDelete(d *Daemon, r *http.Request) response.Response { return resp } - resp = forwardedResponseIfVolumeIsRemote(s, r, poolName, projectName, volumeName, volumeType) + resp = forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } - if volumeType != cluster.StoragePoolVolumeTypeCustom && volumeType != cluster.StoragePoolVolumeTypeImage { - return response.BadRequest(fmt.Errorf("Storage volumes of type %q cannot be deleted with the storage API", volumeTypeName)) - } - - // Get the storage pool the storage volume is supposed to be attached to. - pool, err := storagePools.LoadByName(s, poolName) + requestProjectName := request.ProjectParam(r) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } + if details.volumeType != cluster.StoragePoolVolumeTypeCustom && details.volumeType != cluster.StoragePoolVolumeTypeImage { + return response.BadRequest(fmt.Errorf("Storage volumes of type %q cannot be deleted with the storage API", details.volumeTypeName)) + } + // Get the storage volume. var dbVolume *db.StorageVolume err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - dbVolume, err = tx.GetStoragePoolVolume(ctx, pool.ID(), projectName, volumeType, volumeName, true) + dbVolume, err = tx.GetStoragePoolVolume(ctx, details.pool.ID(), effectiveProjectName, details.volumeType, details.volumeName, true) return err }) if err != nil { @@ -2472,7 +2389,7 @@ func storagePoolVolumeDelete(d *Daemon, r *http.Request) response.Response { } if len(volumeUsedBy) > 0 { - if len(volumeUsedBy) != 1 || volumeType != cluster.StoragePoolVolumeTypeImage || !isImageURL(volumeUsedBy[0], dbVolume.Name) { + if len(volumeUsedBy) != 1 || details.volumeType != cluster.StoragePoolVolumeTypeImage || !isImageURL(volumeUsedBy[0], dbVolume.Name) { return response.BadRequest(fmt.Errorf("The storage volume is still in use")) } } @@ -2481,13 +2398,13 @@ func storagePoolVolumeDelete(d *Daemon, r *http.Request) response.Response { op := &operations.Operation{} op.SetRequestor(r) - switch volumeType { + switch details.volumeType { case cluster.StoragePoolVolumeTypeCustom: - err = pool.DeleteCustomVolume(projectName, volumeName, op) + err = details.pool.DeleteCustomVolume(effectiveProjectName, details.volumeName, op) case cluster.StoragePoolVolumeTypeImage: - err = pool.DeleteImage(volumeName, op) + err = details.pool.DeleteImage(details.volumeName, op) default: - return response.BadRequest(fmt.Errorf(`Storage volumes of type %q cannot be deleted with the storage API`, volumeTypeName)) + return response.BadRequest(fmt.Errorf(`Storage volumes of type %q cannot be deleted with the storage API`, details.volumeTypeName)) } if err != nil { @@ -2727,3 +2644,181 @@ func createStoragePoolVolumeFromBackup(s *state.State, r *http.Request, requestP revert.Success() return operations.OperationResponse(op) } + +// ctxStorageVolumeDetails is the request.CtxKey corresponding to storageVolumeDetails, which is added to the request +// context in addStoragePoolVolumeDetailsToRequestContext. +const ctxStorageVolumeDetails request.CtxKey = "storage-volume-details" + +// storageVolumeDetails contains details common to all storage volume requests. A value of this type is added to the +// request context when addStoragePoolVolumeDetailsToRequestContext is called. We do this to avoid repeated logic when +// parsing the request details and/or making database calls to get the storage pool or effective project. These fields +// are required for the storage volume access check, and are subsequently available in the storage volume handlers. +type storageVolumeDetails struct { + volumeName string + volumeTypeName string + volumeType int + pool storagePools.Pool + forwardingNodeInfo *db.NodeInfo +} + +// addStoragePoolVolumeDetailsToRequestContext extracts storageVolumeDetails from the http.Request and adds it to the +// request context with the ctxStorageVolumeDetails request.CtxKey. Additionally, the effective project of the storage +// bucket is added to the request context under request.CtxEffectiveProjectName. +func addStoragePoolVolumeDetailsToRequestContext(s *state.State, r *http.Request) error { + var details storageVolumeDetails + defer func() { + request.SetCtxValue(r, ctxStorageVolumeDetails, details) + }() + + volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) + if err != nil { + return err + } + + details.volumeName = volumeName + + if shared.IsSnapshot(volumeName) { + return api.StatusErrorf(http.StatusBadRequest, "Invalid storage volume %q", volumeName) + } + + volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) + if err != nil { + return err + } + + details.volumeTypeName = volumeTypeName + + // Convert the volume type name to our internal integer representation. + volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) + if err != nil { + return api.StatusErrorf(http.StatusBadRequest, err.Error()) + } + + details.volumeType = volumeType + + // Get the name of the storage pool the volume is supposed to be attached to. + poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) + if err != nil { + return err + } + + // Load the storage pool containing the volume. This is required by the access handler as all remote volumes + // do not have a location (regardless of whether the caller used a target parameter to send the request to a + // particular member). + storagePool, err := storagePools.LoadByName(s, poolName) + if err != nil { + return err + } + + details.pool = storagePool + + // Get the effective project. + effectiveProject, err := project.StorageVolumeProject(s.DB.Cluster, request.ProjectParam(r), volumeType) + if err != nil { + return fmt.Errorf("Failed to get effective project name: %w", err) + } + + request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProject) + + // If the target is set, we have all the information we need to perform the access check. + if request.QueryParam(r, "target") != "" { + return nil + } + + // If the request has already been forwarded, no reason to perform further logic to determine the location of the + // volume. + _, err = request.GetCtxValue[string](r.Context(), request.CtxForwardedProtocol) + if err == nil { + return nil + } + + // Get information about the cluster member containing the volume. + remoteNodeInfo, err := getRemoteVolumeNodeInfo(r.Context(), s, poolName, effectiveProject, volumeName, volumeType) + if err != nil { + return err + } + + details.forwardingNodeInfo = remoteNodeInfo + + return nil +} + +// getRemoteVolumeNodeInfo figures out the cluster member on which the volume with the given name is defined. If it is +// the local cluster member it returns nil and no error. If it is another cluster member it returns a db.NodeInfo containing +// the name and address of the remote member. If there is more than one cluster member with a matching volume name, an +// error is returned. +func getRemoteVolumeNodeInfo(ctx context.Context, s *state.State, poolName string, projectName string, volumeName string, volumeType int) (*db.NodeInfo, error) { + localNodeID := s.DB.Cluster.GetNodeID() + var err error + var nodes []db.NodeInfo + var poolID int64 + var dbVolume *db.StorageVolume + err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error { + poolID, err = tx.GetStoragePoolID(ctx, poolName) + if err != nil { + return err + } + + nodes, err = tx.GetStorageVolumeNodes(ctx, poolID, projectName, volumeName, volumeType) + if err != nil && !errors.Is(err, db.ErrNoClusterMember) { + return err + } else if err == nil { + return nil + } + + // If we couldn't get the nodes directly, get the volume for a subsequent check. + dbVolume, err = tx.GetStoragePoolVolume(ctx, poolID, projectName, volumeType, volumeName, true) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + // If volume uses a remote storage driver and so has no explicit cluster member, then we need to check + // whether it is exclusively attached to remote instance, and if so then we need to forward the request to + // the node where it is currently used. This avoids conflicting with another member when using it locally. + if dbVolume != nil { + remoteInstance, err := storagePools.VolumeUsedByExclusiveRemoteInstancesWithProfiles(s, poolName, projectName, &dbVolume.StorageVolume) + if err != nil { + return nil, fmt.Errorf("Failed checking if volume %q is available: %w", volumeName, err) + } + + if remoteInstance == nil { + // Volume isn't exclusively attached to an instance. Use local cluster member. + return nil, nil + } + + var instNode db.NodeInfo + err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error { + instNode, err = tx.GetNodeByName(ctx, remoteInstance.Node) + return err + }) + if err != nil { + return nil, fmt.Errorf("Failed getting cluster member info for %q: %w", remoteInstance.Node, err) + } + + // Replace node list with instance's cluster member node (which might be local member). + nodes = []db.NodeInfo{instNode} + } + + nodeCount := len(nodes) + if nodeCount > 1 { + return nil, fmt.Errorf("More than one cluster member has a volume named %q. Please target a specific member", volumeName) + } else if nodeCount < 1 { + // Should never get here. + return nil, fmt.Errorf("Volume %q has empty cluster member list", volumeName) + } + + node := nodes[0] + if node.ID == localNodeID { + // Use local cluster member if volume belongs to this local member. + return nil, nil + } + + // Connect to remote cluster member. + return &node, nil +} diff --git a/lxd/storage_volumes_backup.go b/lxd/storage_volumes_backup.go index 6fc361e339da..d84a4e96e72b 100644 --- a/lxd/storage_volumes_backup.go +++ b/lxd/storage_volumes_backup.go @@ -21,11 +21,9 @@ import ( "github.com/canonical/lxd/lxd/project" "github.com/canonical/lxd/lxd/request" "github.com/canonical/lxd/lxd/response" - storagePools "github.com/canonical/lxd/lxd/storage" "github.com/canonical/lxd/lxd/util" "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/api" - "github.com/canonical/lxd/shared/entity" "github.com/canonical/lxd/shared/logger" "github.com/canonical/lxd/shared/version" ) @@ -33,22 +31,22 @@ import ( var storagePoolVolumeTypeCustomBackupsCmd = APIEndpoint{ Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}/backups", - Get: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupsGet, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanView, "poolName", "type", "volumeName")}, - Post: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupsPost, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanManageBackups, "poolName", "type", "volumeName")}, + Get: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupsGet, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanView)}, + Post: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupsPost, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanManageBackups)}, } var storagePoolVolumeTypeCustomBackupCmd = APIEndpoint{ Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}/backups/{backupName}", - Get: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupGet, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanView, "poolName", "type", "volumeName")}, - Post: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupPost, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanManageBackups, "poolName", "type", "volumeName")}, - Delete: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupDelete, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanManageBackups, "poolName", "type", "volumeName")}, + Get: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupGet, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanView)}, + Post: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupPost, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanManageBackups)}, + Delete: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupDelete, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanManageBackups)}, } var storagePoolVolumeTypeCustomBackupExportCmd = APIEndpoint{ Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}/backups/{backupName}/export", - Get: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupExportGet, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanView, "poolName", "type", "volumeName")}, + Get: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupExportGet, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanView)}, } // swagger:operation GET /1.0/storage-pools/{poolName}/volumes/{type}/{volumeName}/backups storage storage_pool_volumes_type_backups_get @@ -156,56 +154,23 @@ var storagePoolVolumeTypeCustomBackupExportCmd = APIEndpoint{ func storagePoolVolumeTypeCustomBackupsGet(d *Daemon, r *http.Request) response.Response { s := d.State() - requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, cluster.StoragePoolVolumeTypeCustom) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the storage pool the volume is supposed to be attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - // Get the volume type. - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - // Check that the storage volume type is valid. - if volumeType != cluster.StoragePoolVolumeTypeCustom { - return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName)) - } - - var poolID int64 - - err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - var err error - - poolID, _, _, err = tx.GetStoragePool(ctx, poolName) - - return err - }) - if err != nil { - return response.SmartError(err) + if details.volumeType != cluster.StoragePoolVolumeTypeCustom { + return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", details.volumeTypeName)) } // Handle requests targeted to a volume on a different node - resp := forwardedResponseIfVolumeIsRemote(s, r, poolName, projectName, volumeName, cluster.StoragePoolVolumeTypeCustom) + resp := forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } @@ -215,7 +180,7 @@ func storagePoolVolumeTypeCustomBackupsGet(d *Daemon, r *http.Request) response. var volumeBackups []db.StoragePoolVolumeBackup err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - volumeBackups, err = tx.GetStoragePoolVolumeBackups(ctx, projectName, volumeName, poolID) + volumeBackups, err = tx.GetStoragePoolVolumeBackups(ctx, effectiveProjectName, details.volumeName, details.pool.ID()) return err }) if err != nil { @@ -225,7 +190,7 @@ func storagePoolVolumeTypeCustomBackupsGet(d *Daemon, r *http.Request) response. backups := make([]*backup.VolumeBackup, len(volumeBackups)) for i, b := range volumeBackups { - backups[i] = backup.NewVolumeBackup(s, projectName, poolName, volumeName, b.ID, b.Name, b.CreationDate, b.ExpiryDate, b.VolumeOnly, b.OptimizedStorage) + backups[i] = backup.NewVolumeBackup(s, effectiveProjectName, details.pool.Name(), details.volumeName, b.ID, b.Name, b.CreationDate, b.ExpiryDate, b.VolumeOnly, b.OptimizedStorage) } resultString := []string{} @@ -233,7 +198,7 @@ func storagePoolVolumeTypeCustomBackupsGet(d *Daemon, r *http.Request) response. for _, backup := range backups { if !recursion { - url := api.NewURL().Path(version.APIVersion, "storage-pools", poolName, "volumes", "custom", volumeName, "backups", strings.Split(backup.Name(), "/")[1]).String() + url := api.NewURL().Path(version.APIVersion, "storage-pools", details.pool.Name(), "volumes", "custom", details.volumeName, "backups", strings.Split(backup.Name(), "/")[1]).String() resultString = append(resultString, url) } else { render := backup.Render() @@ -288,43 +253,24 @@ func storagePoolVolumeTypeCustomBackupsGet(d *Daemon, r *http.Request) response. func storagePoolVolumeTypeCustomBackupsPost(d *Daemon, r *http.Request) response.Response { s := d.State() - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the storage pool the volume is supposed to be attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the volume type. - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - // Check that the storage volume type is valid. - if volumeType != cluster.StoragePoolVolumeTypeCustom { - return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName)) + if details.volumeType != cluster.StoragePoolVolumeTypeCustom { + return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", details.volumeTypeName)) } requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, cluster.StoragePoolVolumeTypeCustom) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - err := project.AllowBackupCreation(tx, projectName) + err := project.AllowBackupCreation(tx, effectiveProjectName) return err }) if err != nil { @@ -336,27 +282,14 @@ func storagePoolVolumeTypeCustomBackupsPost(d *Daemon, r *http.Request) response return resp } - var poolID int64 - - err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - var err error - - poolID, _, _, err = tx.GetStoragePool(ctx, poolName) - - return err - }) - if err != nil { - return response.SmartError(err) - } - - resp = forwardedResponseIfVolumeIsRemote(s, r, poolName, projectName, volumeName, cluster.StoragePoolVolumeTypeCustom) + resp = forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } var dbVolume *db.StorageVolume err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - dbVolume, err = tx.GetStoragePoolVolume(ctx, poolID, projectName, volumeType, volumeName, true) + dbVolume, err = tx.GetStoragePoolVolume(ctx, details.pool.ID(), effectiveProjectName, details.volumeType, details.volumeName, true) return err }) if err != nil { @@ -393,14 +326,14 @@ func storagePoolVolumeTypeCustomBackupsPost(d *Daemon, r *http.Request) response // come up with a name. err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - backups, err = tx.GetStoragePoolVolumeBackupsNames(ctx, projectName, volumeName, poolID) + backups, err = tx.GetStoragePoolVolumeBackupsNames(ctx, effectiveProjectName, details.volumeName, details.pool.ID()) return err }) if err != nil { return response.BadRequest(err) } - base := volumeName + shared.SnapshotDelimiter + "backup" + base := details.volumeName + shared.SnapshotDelimiter + "backup" length := len(base) max := 0 @@ -430,7 +363,7 @@ func storagePoolVolumeTypeCustomBackupsPost(d *Daemon, r *http.Request) response return response.BadRequest(fmt.Errorf("Backup names may not contain slashes")) } - fullName := volumeName + shared.SnapshotDelimiter + req.Name + fullName := details.volumeName + shared.SnapshotDelimiter + req.Name volumeOnly := req.VolumeOnly backup := func(op *operations.Operation) error { @@ -444,19 +377,19 @@ func storagePoolVolumeTypeCustomBackupsPost(d *Daemon, r *http.Request) response CompressionAlgorithm: req.CompressionAlgorithm, } - err := volumeBackupCreate(s, args, projectName, poolName, volumeName) + err := volumeBackupCreate(s, args, effectiveProjectName, details.pool.Name(), details.volumeName) if err != nil { return fmt.Errorf("Create volume backup: %w", err) } - s.Events.SendLifecycle(projectName, lifecycle.StorageVolumeBackupCreated.Event(poolName, volumeTypeName, args.Name, projectName, op.Requestor(), logger.Ctx{"type": volumeTypeName})) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.StorageVolumeBackupCreated.Event(details.pool.Name(), details.volumeTypeName, args.Name, effectiveProjectName, op.Requestor(), logger.Ctx{"type": details.volumeTypeName})) return nil } resources := map[string][]api.URL{} - resources["storage_volumes"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", poolName, "volumes", volumeTypeName, volumeName)} - resources["backups"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", poolName, "volumes", volumeTypeName, volumeName, "backups", req.Name)} + resources["storage_volumes"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", details.pool.Name(), "volumes", details.volumeTypeName, details.volumeName)} + resources["backups"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", details.pool.Name(), "volumes", details.volumeTypeName, details.volumeName, "backups", req.Name)} op, err := operations.OperationCreate(s, requestProjectName, operations.OperationClassTask, operationtype.CustomVolumeBackupCreate, resources, nil, backup, nil, nil, r) if err != nil { @@ -514,20 +447,7 @@ func storagePoolVolumeTypeCustomBackupsPost(d *Daemon, r *http.Request) response func storagePoolVolumeTypeCustomBackupGet(d *Daemon, r *http.Request) response.Response { s := d.State() - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the storage pool the volume is supposed to be attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the volume type. - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } @@ -538,19 +458,12 @@ func storagePoolVolumeTypeCustomBackupGet(d *Daemon, r *http.Request) response.R return response.SmartError(err) } - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - // Check that the storage volume type is valid. - if volumeType != cluster.StoragePoolVolumeTypeCustom { - return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName)) + if details.volumeType != cluster.StoragePoolVolumeTypeCustom { + return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", details.volumeTypeName)) } - requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, cluster.StoragePoolVolumeTypeCustom) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } @@ -560,14 +473,14 @@ func storagePoolVolumeTypeCustomBackupGet(d *Daemon, r *http.Request) response.R return resp } - resp = forwardedResponseIfVolumeIsRemote(s, r, poolName, projectName, volumeName, cluster.StoragePoolVolumeTypeCustom) + resp = forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } - fullName := volumeName + shared.SnapshotDelimiter + backupName + fullName := details.volumeName + shared.SnapshotDelimiter + backupName - backup, err := storagePoolVolumeBackupLoadByName(s, projectName, poolName, fullName) + backup, err := storagePoolVolumeBackupLoadByName(s, effectiveProjectName, details.pool.Name(), fullName) if err != nil { return response.SmartError(err) } @@ -615,20 +528,7 @@ func storagePoolVolumeTypeCustomBackupGet(d *Daemon, r *http.Request) response.R func storagePoolVolumeTypeCustomBackupPost(d *Daemon, r *http.Request) response.Response { s := d.State() - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the storage pool the volume is supposed to be attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the volume type. - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } @@ -639,19 +539,13 @@ func storagePoolVolumeTypeCustomBackupPost(d *Daemon, r *http.Request) response. return response.SmartError(err) } - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - // Check that the storage volume type is valid. - if volumeType != cluster.StoragePoolVolumeTypeCustom { - return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName)) + if details.volumeType != cluster.StoragePoolVolumeTypeCustom { + return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", details.volumeTypeName)) } requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, cluster.StoragePoolVolumeTypeCustom) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } @@ -661,7 +555,7 @@ func storagePoolVolumeTypeCustomBackupPost(d *Daemon, r *http.Request) response. return resp } - resp = forwardedResponseIfVolumeIsRemote(s, r, poolName, projectName, volumeName, cluster.StoragePoolVolumeTypeCustom) + resp = forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } @@ -677,14 +571,14 @@ func storagePoolVolumeTypeCustomBackupPost(d *Daemon, r *http.Request) response. return response.BadRequest(fmt.Errorf("Backup names may not contain slashes")) } - oldName := volumeName + shared.SnapshotDelimiter + backupName + oldName := details.volumeName + shared.SnapshotDelimiter + backupName - backup, err := storagePoolVolumeBackupLoadByName(s, projectName, poolName, oldName) + backup, err := storagePoolVolumeBackupLoadByName(s, effectiveProjectName, details.pool.Name(), oldName) if err != nil { return response.SmartError(err) } - newName := volumeName + shared.SnapshotDelimiter + req.Name + newName := details.volumeName + shared.SnapshotDelimiter + req.Name rename := func(op *operations.Operation) error { err := backup.Rename(newName) @@ -692,14 +586,14 @@ func storagePoolVolumeTypeCustomBackupPost(d *Daemon, r *http.Request) response. return err } - s.Events.SendLifecycle(projectName, lifecycle.StorageVolumeBackupRenamed.Event(poolName, volumeTypeName, newName, projectName, op.Requestor(), logger.Ctx{"old_name": oldName})) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.StorageVolumeBackupRenamed.Event(details.pool.Name(), details.volumeTypeName, newName, effectiveProjectName, op.Requestor(), logger.Ctx{"old_name": oldName})) return nil } resources := map[string][]api.URL{} - resources["storage_volumes"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", poolName, "volumes", volumeTypeName, volumeName)} - resources["backups"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", poolName, "volumes", volumeTypeName, volumeName, "backups", oldName)} + resources["storage_volumes"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", details.pool.Name(), "volumes", details.volumeTypeName, details.volumeName)} + resources["backups"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", details.pool.Name(), "volumes", details.volumeTypeName, details.volumeName, "backups", oldName)} op, err := operations.OperationCreate(s, requestProjectName, operations.OperationClassTask, operationtype.CustomVolumeBackupRename, resources, nil, rename, nil, nil, r) if err != nil { @@ -743,20 +637,7 @@ func storagePoolVolumeTypeCustomBackupPost(d *Daemon, r *http.Request) response. func storagePoolVolumeTypeCustomBackupDelete(d *Daemon, r *http.Request) response.Response { s := d.State() - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the storage pool the volume is supposed to be attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the volume type. - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } @@ -767,19 +648,13 @@ func storagePoolVolumeTypeCustomBackupDelete(d *Daemon, r *http.Request) respons return response.SmartError(err) } - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - // Check that the storage volume type is valid. - if volumeType != cluster.StoragePoolVolumeTypeCustom { - return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName)) + if details.volumeType != cluster.StoragePoolVolumeTypeCustom { + return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", details.volumeTypeName)) } requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, cluster.StoragePoolVolumeTypeCustom) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } @@ -789,14 +664,14 @@ func storagePoolVolumeTypeCustomBackupDelete(d *Daemon, r *http.Request) respons return resp } - resp = forwardedResponseIfVolumeIsRemote(s, r, poolName, projectName, volumeName, cluster.StoragePoolVolumeTypeCustom) + resp = forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } - fullName := volumeName + shared.SnapshotDelimiter + backupName + fullName := details.volumeName + shared.SnapshotDelimiter + backupName - backup, err := storagePoolVolumeBackupLoadByName(s, projectName, poolName, fullName) + backup, err := storagePoolVolumeBackupLoadByName(s, effectiveProjectName, details.pool.Name(), fullName) if err != nil { return response.SmartError(err) } @@ -807,14 +682,14 @@ func storagePoolVolumeTypeCustomBackupDelete(d *Daemon, r *http.Request) respons return err } - s.Events.SendLifecycle(projectName, lifecycle.StorageVolumeBackupDeleted.Event(poolName, volumeTypeName, fullName, projectName, op.Requestor(), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.StorageVolumeBackupDeleted.Event(details.pool.Name(), details.volumeTypeName, fullName, effectiveProjectName, op.Requestor(), nil)) return nil } resources := map[string][]api.URL{} - resources["storage_volumes"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", poolName, "volumes", volumeTypeName, volumeName)} - resources["backups"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", poolName, "volumes", volumeTypeName, volumeName, "backups", backupName)} + resources["storage_volumes"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", details.pool.Name(), "volumes", details.volumeTypeName, details.volumeName)} + resources["backups"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", details.pool.Name(), "volumes", details.volumeTypeName, details.volumeName, "backups", backupName)} op, err := operations.OperationCreate(s, requestProjectName, operations.OperationClassTask, operationtype.CustomVolumeBackupRemove, resources, nil, remove, nil, nil, r) if err != nil { @@ -854,20 +729,7 @@ func storagePoolVolumeTypeCustomBackupDelete(d *Daemon, r *http.Request) respons func storagePoolVolumeTypeCustomBackupExportGet(d *Daemon, r *http.Request) response.Response { s := d.State() - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the storage pool the volume is supposed to be attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the volume type. - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } @@ -878,19 +740,12 @@ func storagePoolVolumeTypeCustomBackupExportGet(d *Daemon, r *http.Request) resp return response.SmartError(err) } - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - // Check that the storage volume type is valid. - if volumeType != cluster.StoragePoolVolumeTypeCustom { - return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName)) + if details.volumeType != cluster.StoragePoolVolumeTypeCustom { + return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", details.volumeTypeName)) } - requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, cluster.StoragePoolVolumeTypeCustom) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } @@ -900,24 +755,24 @@ func storagePoolVolumeTypeCustomBackupExportGet(d *Daemon, r *http.Request) resp return resp } - resp = forwardedResponseIfVolumeIsRemote(s, r, poolName, projectName, volumeName, cluster.StoragePoolVolumeTypeCustom) + resp = forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } - fullName := volumeName + shared.SnapshotDelimiter + backupName + fullName := details.volumeName + shared.SnapshotDelimiter + backupName // Ensure the volume exists - _, err = storagePoolVolumeBackupLoadByName(s, projectName, poolName, fullName) + _, err = storagePoolVolumeBackupLoadByName(s, effectiveProjectName, details.pool.Name(), fullName) if err != nil { return response.SmartError(err) } ent := response.FileResponseEntry{ - Path: shared.VarPath("backups", "custom", poolName, project.StorageVolume(projectName, fullName)), + Path: shared.VarPath("backups", "custom", details.pool.Name(), project.StorageVolume(effectiveProjectName, fullName)), } - s.Events.SendLifecycle(projectName, lifecycle.StorageVolumeBackupRetrieved.Event(poolName, volumeTypeName, fullName, projectName, request.CreateRequestor(r), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.StorageVolumeBackupRetrieved.Event(details.pool.Name(), details.volumeTypeName, fullName, effectiveProjectName, request.CreateRequestor(r), nil)) return response.FileResponse(r, []response.FileResponseEntry{ent}, nil) } diff --git a/lxd/storage_volumes_snapshot.go b/lxd/storage_volumes_snapshot.go index 4bd14f4edd54..292e0daadd65 100644 --- a/lxd/storage_volumes_snapshot.go +++ b/lxd/storage_volumes_snapshot.go @@ -29,7 +29,6 @@ import ( "github.com/canonical/lxd/lxd/util" "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/api" - "github.com/canonical/lxd/shared/entity" "github.com/canonical/lxd/shared/logger" "github.com/canonical/lxd/shared/version" ) @@ -37,18 +36,18 @@ import ( var storagePoolVolumeSnapshotsTypeCmd = APIEndpoint{ Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}/snapshots", - Get: APIEndpointAction{Handler: storagePoolVolumeSnapshotsTypeGet, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanView, "poolName", "type", "volumeName")}, - Post: APIEndpointAction{Handler: storagePoolVolumeSnapshotsTypePost, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanManageSnapshots, "poolName", "type", "volumeName")}, + Get: APIEndpointAction{Handler: storagePoolVolumeSnapshotsTypeGet, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanView)}, + Post: APIEndpointAction{Handler: storagePoolVolumeSnapshotsTypePost, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanManageSnapshots)}, } var storagePoolVolumeSnapshotTypeCmd = APIEndpoint{ Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}/snapshots/{snapshotName}", - Delete: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypeDelete, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanManageSnapshots, "poolName", "type", "volumeName")}, - Get: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypeGet, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanView, "poolName", "type", "volumeName")}, - Post: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypePost, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanManageSnapshots, "poolName", "type", "volumeName")}, - Patch: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypePatch, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanManageSnapshots, "poolName", "type", "volumeName")}, - Put: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypePut, AccessHandler: allowPermission(entity.TypeStorageVolume, auth.EntitlementCanManageSnapshots, "poolName", "type", "volumeName")}, + Delete: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypeDelete, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanManageSnapshots)}, + Get: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypeGet, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanView)}, + Post: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypePost, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanManageSnapshots)}, + Patch: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypePatch, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanManageSnapshots)}, + Put: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypePut, AccessHandler: storagePoolVolumeTypeAccessHandler(auth.EntitlementCanManageSnapshots)}, } // swagger:operation POST /1.0/storage-pools/{poolName}/volumes/{type}/{volumeName}/snapshots storage storage_pool_volumes_type_snapshots_post @@ -91,44 +90,24 @@ var storagePoolVolumeSnapshotTypeCmd = APIEndpoint{ func storagePoolVolumeSnapshotsTypePost(d *Daemon, r *http.Request) response.Response { s := d.State() - // Get the name of the pool. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } - // Get the name of the volume type. - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) - if err != nil { - return response.SmartError(err) - } - - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - // Check that the storage volume type is valid. - if volumeType != dbCluster.StoragePoolVolumeTypeCustom { - return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName)) + if details.volumeType != dbCluster.StoragePoolVolumeTypeCustom { + return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", details.volumeTypeName)) } - // Get the project name. requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, volumeType) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - dbProject, err := dbCluster.GetProject(context.Background(), tx.Tx(), projectName) + dbProject, err := dbCluster.GetProject(context.Background(), tx.Tx(), effectiveProjectName) if err != nil { return err } @@ -155,7 +134,7 @@ func storagePoolVolumeSnapshotsTypePost(d *Daemon, r *http.Request) response.Res return resp } - resp = forwardedResponseIfVolumeIsRemote(s, r, poolName, projectName, volumeName, volumeType) + resp = forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } @@ -168,7 +147,7 @@ func storagePoolVolumeSnapshotsTypePost(d *Daemon, r *http.Request) response.Res } // Check that this isn't a restricted volume - used, err := storagePools.VolumeUsedByDaemon(s, poolName, volumeName) + used, err := storagePools.VolumeUsedByDaemon(s, details.pool.Name(), details.volumeName) if err != nil { return response.InternalError(err) } @@ -177,17 +156,11 @@ func storagePoolVolumeSnapshotsTypePost(d *Daemon, r *http.Request) response.Res return response.BadRequest(fmt.Errorf("Volumes used by LXD itself cannot have snapshots")) } - // Retrieve the storage pool (and check if the storage pool exists). - pool, err := storagePools.LoadByName(s, poolName) - if err != nil { - return response.SmartError(err) - } - var parentDBVolume *db.StorageVolume var parentVolumeArgs db.StorageVolumeArgs err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { // Get the parent volume so we can get the config. - parentDBVolume, err = tx.GetStoragePoolVolume(ctx, pool.ID(), projectName, volumeType, volumeName, true) + parentDBVolume, err = tx.GetStoragePoolVolume(ctx, details.pool.ID(), effectiveProjectName, details.volumeType, details.volumeName, true) if err != nil { return err } @@ -216,14 +189,14 @@ func storagePoolVolumeSnapshotsTypePost(d *Daemon, r *http.Request) response.Res } // Validate the snapshot name using same rule as pool name. - err = pool.ValidateName(req.Name) + err = details.pool.ValidateName(req.Name) if err != nil { return response.BadRequest(err) } err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { // Ensure that the snapshot doesn't already exist. - snapDBVolume, err := tx.GetStoragePoolVolume(ctx, pool.ID(), projectName, volumeType, fmt.Sprintf("%s/%s", volumeName, req.Name), true) + snapDBVolume, err := tx.GetStoragePoolVolume(ctx, details.pool.ID(), effectiveProjectName, details.volumeType, fmt.Sprintf("%s/%s", details.volumeName, req.Name), true) if err != nil && !response.IsNotFoundError(err) { return err } else if snapDBVolume != nil { @@ -249,12 +222,12 @@ func storagePoolVolumeSnapshotsTypePost(d *Daemon, r *http.Request) response.Res // Create the snapshot. snapshot := func(op *operations.Operation) error { - return pool.CreateCustomVolumeSnapshot(projectName, volumeName, req.Name, expiry, op) + return details.pool.CreateCustomVolumeSnapshot(effectiveProjectName, details.volumeName, req.Name, expiry, op) } resources := map[string][]api.URL{} - resources["storage_volumes"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", poolName, "volumes", volumeTypeName, volumeName)} - resources["storage_volume_snapshots"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", poolName, "volumes", volumeTypeName, volumeName, "snapshots", req.Name)} + resources["storage_volumes"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", details.pool.Name(), "volumes", details.volumeTypeName, details.volumeName)} + resources["storage_volume_snapshots"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", details.pool.Name(), "volumes", details.volumeTypeName, details.volumeName, "snapshots", req.Name)} op, err := operations.OperationCreate(s, requestProjectName, operations.OperationClassTask, operationtype.VolumeSnapshotCreate, resources, nil, snapshot, nil, nil, r) if err != nil { @@ -369,57 +342,30 @@ func storagePoolVolumeSnapshotsTypePost(d *Daemon, r *http.Request) response.Res func storagePoolVolumeSnapshotsTypeGet(d *Daemon, r *http.Request) response.Response { s := d.State() - // Get the name of the pool the storage volume is supposed to be attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } recursion := util.IsRecursionRequest(r) - // Get the name of the volume type. - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the volume type. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) - if err != nil { - return response.SmartError(err) - } - - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - // Check that the storage volume type is valid. - if !shared.ValueInSlice(volumeType, supportedVolumeTypes) { - return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName)) + if !shared.ValueInSlice(details.volumeType, supportedVolumeTypes) { + return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", details.volumeTypeName)) } - requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, volumeType) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - var poolID int64 var volumes []db.StorageVolumeArgs err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { var err error - // Retrieve ID of the storage pool (and check if the storage pool exists). - poolID, err = tx.GetStoragePoolID(ctx, poolName) - if err != nil { - return err - } - // Get the names of all storage volume snapshots of a given volume. - volumes, err = tx.GetLocalStoragePoolVolumeSnapshotsWithType(ctx, projectName, volumeName, volumeType, poolID) + volumes, err = tx.GetLocalStoragePoolVolumeSnapshotsWithType(ctx, effectiveProjectName, details.volumeName, details.volumeType, details.pool.ID()) if err != nil { return err } @@ -437,18 +383,18 @@ func storagePoolVolumeSnapshotsTypeGet(d *Daemon, r *http.Request) response.Resp _, snapshotName, _ := api.GetParentAndSnapshotName(volume.Name) if !recursion { - resultString = append(resultString, fmt.Sprintf("/%s/storage-pools/%s/volumes/%s/%s/snapshots/%s", version.APIVersion, poolName, volumeTypeName, volumeName, snapshotName)) + resultString = append(resultString, fmt.Sprintf("/%s/storage-pools/%s/volumes/%s/%s/snapshots/%s", version.APIVersion, details.pool.Name(), details.volumeTypeName, details.volumeName, snapshotName)) } else { var vol *db.StorageVolume err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - vol, err = tx.GetStoragePoolVolume(ctx, poolID, projectName, volumeType, volume.Name, true) + vol, err = tx.GetStoragePoolVolume(ctx, details.pool.ID(), effectiveProjectName, details.volumeType, volume.Name, true) return err }) if err != nil { return response.SmartError(err) } - volumeUsedBy, err := storagePoolVolumeUsedByGet(s, projectName, vol) + volumeUsedBy, err := storagePoolVolumeUsedByGet(s, effectiveProjectName, vol) if err != nil { return response.SmartError(err) } @@ -517,20 +463,7 @@ func storagePoolVolumeSnapshotsTypeGet(d *Daemon, r *http.Request) response.Resp func storagePoolVolumeSnapshotTypePost(d *Daemon, r *http.Request) response.Response { s := d.State() - // Get the name of the storage pool the volume is supposed to be attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the volume type. - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } @@ -541,20 +474,13 @@ func storagePoolVolumeSnapshotTypePost(d *Daemon, r *http.Request) response.Resp return response.SmartError(err) } - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - // Check that the storage volume type is valid. - if volumeType != dbCluster.StoragePoolVolumeTypeCustom { - return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName)) + if details.volumeType != dbCluster.StoragePoolVolumeTypeCustom { + return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", details.volumeTypeName)) } - // Get the project name. requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, volumeType) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } @@ -565,12 +491,13 @@ func storagePoolVolumeSnapshotTypePost(d *Daemon, r *http.Request) response.Resp return resp } - fullSnapshotName := fmt.Sprintf("%s/%s", volumeName, snapshotName) - resp = forwardedResponseIfVolumeIsRemote(s, r, poolName, projectName, volumeName, volumeType) + resp = forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } + fullSnapshotName := fmt.Sprintf("%s/%s", details.volumeName, snapshotName) + // Parse the request. req := api.StorageVolumeSnapshotPost{} err = json.NewDecoder(r.Body).Decode(&req) @@ -591,21 +518,16 @@ func storagePoolVolumeSnapshotTypePost(d *Daemon, r *http.Request) response.Resp Target: req.Target, } - return storagePoolVolumeTypePostMigration(s, r, requestProjectName, projectName, poolName, fullSnapshotName, req) + return storagePoolVolumeTypePostMigration(s, r, requestProjectName, effectiveProjectName, details.pool.Name(), fullSnapshotName, req) } // Rename the snapshot. snapshotRename := func(op *operations.Operation) error { - pool, err := storagePools.LoadByName(s, poolName) - if err != nil { - return err - } - - return pool.RenameCustomVolumeSnapshot(projectName, fullSnapshotName, req.Name, op) + return details.pool.RenameCustomVolumeSnapshot(effectiveProjectName, fullSnapshotName, req.Name, op) } resources := map[string][]api.URL{} - resources["storage_volume_snapshots"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", poolName, "volumes", volumeTypeName, volumeName, "snapshots", snapshotName)} + resources["storage_volume_snapshots"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", details.pool.Name(), "volumes", details.volumeTypeName, details.volumeName, "snapshots", snapshotName)} op, err := operations.OperationCreate(s, requestProjectName, operations.OperationClassTask, operationtype.VolumeSnapshotRename, resources, nil, snapshotRename, nil, nil, r) if err != nil { @@ -663,21 +585,7 @@ func storagePoolVolumeSnapshotTypePost(d *Daemon, r *http.Request) response.Resp func storagePoolVolumeSnapshotTypeGet(d *Daemon, r *http.Request) response.Response { s := d.State() - // Get the name of the storage pool the volume is supposed to be - // attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the volume type. - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } @@ -688,15 +596,7 @@ func storagePoolVolumeSnapshotTypeGet(d *Daemon, r *http.Request) response.Respo return response.SmartError(err) } - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - - // Get the project name. - requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, volumeType) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } @@ -707,24 +607,18 @@ func storagePoolVolumeSnapshotTypeGet(d *Daemon, r *http.Request) response.Respo return resp } - fullSnapshotName := fmt.Sprintf("%s/%s", volumeName, snapshotName) - resp = forwardedResponseIfVolumeIsRemote(s, r, poolName, projectName, fullSnapshotName, volumeType) + resp = forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } - var poolID int64 + fullSnapshotName := fmt.Sprintf("%s/%s", details.volumeName, snapshotName) + var dbVolume *db.StorageVolume var expiry time.Time err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - // Get the snapshot. - poolID, _, _, err = tx.GetStoragePool(ctx, poolName) - if err != nil { - return err - } - - dbVolume, err = tx.GetStoragePoolVolume(ctx, poolID, projectName, volumeType, fullSnapshotName, true) + dbVolume, err = tx.GetStoragePoolVolume(ctx, details.pool.ID(), effectiveProjectName, details.volumeType, fullSnapshotName, true) if err != nil { return err } @@ -794,21 +688,7 @@ func storagePoolVolumeSnapshotTypeGet(d *Daemon, r *http.Request) response.Respo func storagePoolVolumeSnapshotTypePut(d *Daemon, r *http.Request) response.Response { s := d.State() - // Get the name of the storage pool the volume is supposed to be - // attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the volume type. - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } @@ -819,15 +699,7 @@ func storagePoolVolumeSnapshotTypePut(d *Daemon, r *http.Request) response.Respo return response.SmartError(err) } - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - - // Get the project name. - requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, volumeType) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } @@ -838,24 +710,18 @@ func storagePoolVolumeSnapshotTypePut(d *Daemon, r *http.Request) response.Respo return resp } - fullSnapshotName := fmt.Sprintf("%s/%s", volumeName, snapshotName) - resp = forwardedResponseIfVolumeIsRemote(s, r, poolName, projectName, fullSnapshotName, volumeType) + resp = forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } - var poolID int64 + fullSnapshotName := fmt.Sprintf("%s/%s", details.volumeName, snapshotName) + var dbVolume *db.StorageVolume var expiry time.Time err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - // Get the snapshot. - poolID, _, _, err = tx.GetStoragePool(ctx, poolName) - if err != nil { - return err - } - - dbVolume, err = tx.GetStoragePoolVolume(ctx, poolID, projectName, volumeType, fullSnapshotName, true) + dbVolume, err = tx.GetStoragePoolVolume(ctx, details.pool.ID(), effectiveProjectName, details.volumeType, fullSnapshotName, true) if err != nil { return err } @@ -885,7 +751,7 @@ func storagePoolVolumeSnapshotTypePut(d *Daemon, r *http.Request) response.Respo return response.BadRequest(err) } - return doStoragePoolVolumeSnapshotUpdate(s, r, poolName, projectName, dbVolume.Name, volumeType, req) + return doStoragePoolVolumeSnapshotUpdate(s, r, effectiveProjectName, dbVolume.Name, details.volumeType, req) } // swagger:operation PATCH /1.0/storage-pools/{poolName}/volumes/{type}/{volumeName}/snapshots/{snapshotName} storage storage_pool_volumes_type_snapshot_patch @@ -930,21 +796,7 @@ func storagePoolVolumeSnapshotTypePut(d *Daemon, r *http.Request) response.Respo func storagePoolVolumeSnapshotTypePatch(d *Daemon, r *http.Request) response.Response { s := d.State() - // Get the name of the storage pool the volume is supposed to be - // attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the volume type. - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } @@ -955,15 +807,7 @@ func storagePoolVolumeSnapshotTypePatch(d *Daemon, r *http.Request) response.Res return response.SmartError(err) } - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - - // Get the project name. - requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, volumeType) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } @@ -974,24 +818,18 @@ func storagePoolVolumeSnapshotTypePatch(d *Daemon, r *http.Request) response.Res return resp } - fullSnapshotName := fmt.Sprintf("%s/%s", volumeName, snapshotName) - resp = forwardedResponseIfVolumeIsRemote(s, r, poolName, projectName, fullSnapshotName, volumeType) + resp = forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } - var poolID int64 + fullSnapshotName := fmt.Sprintf("%s/%s", details.volumeName, snapshotName) + var dbVolume *db.StorageVolume var expiry time.Time err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - // Get the snapshot. - poolID, _, _, err = tx.GetStoragePool(ctx, poolName) - if err != nil { - return err - } - - dbVolume, err = tx.GetStoragePoolVolume(ctx, poolID, projectName, volumeType, fullSnapshotName, true) + dbVolume, err = tx.GetStoragePoolVolume(ctx, details.pool.ID(), effectiveProjectName, details.volumeType, fullSnapshotName, true) if err != nil { return err } @@ -1024,16 +862,16 @@ func storagePoolVolumeSnapshotTypePatch(d *Daemon, r *http.Request) response.Res return response.BadRequest(err) } - return doStoragePoolVolumeSnapshotUpdate(s, r, poolName, projectName, dbVolume.Name, volumeType, req) + return doStoragePoolVolumeSnapshotUpdate(s, r, effectiveProjectName, dbVolume.Name, details.volumeType, req) } -func doStoragePoolVolumeSnapshotUpdate(s *state.State, r *http.Request, poolName string, projectName string, volName string, volumeType int, req api.StorageVolumeSnapshotPut) response.Response { +func doStoragePoolVolumeSnapshotUpdate(s *state.State, r *http.Request, projectName string, volName string, volumeType int, req api.StorageVolumeSnapshotPut) response.Response { expiry := time.Time{} if req.ExpiresAt != nil { expiry = *req.ExpiresAt } - pool, err := storagePools.LoadByName(s, poolName) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } @@ -1044,7 +882,7 @@ func doStoragePoolVolumeSnapshotUpdate(s *state.State, r *http.Request, poolName // Update the database. if volumeType == dbCluster.StoragePoolVolumeTypeCustom { - err = pool.UpdateCustomVolumeSnapshot(projectName, volName, req.Description, nil, expiry, op) + err = details.pool.UpdateCustomVolumeSnapshot(projectName, volName, req.Description, nil, expiry, op) if err != nil { return response.SmartError(err) } @@ -1054,7 +892,7 @@ func doStoragePoolVolumeSnapshotUpdate(s *state.State, r *http.Request, poolName return response.SmartError(err) } - err = pool.UpdateInstanceSnapshot(inst, req.Description, nil, op) + err = details.pool.UpdateInstanceSnapshot(inst, req.Description, nil, op) if err != nil { return response.SmartError(err) } @@ -1097,20 +935,7 @@ func doStoragePoolVolumeSnapshotUpdate(s *state.State, r *http.Request, poolName func storagePoolVolumeSnapshotTypeDelete(d *Daemon, r *http.Request) response.Response { s := d.State() - // Get the name of the storage pool the volume is supposed to be attached to. - poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the volume type. - volumeTypeName, err := url.PathUnescape(mux.Vars(r)["type"]) - if err != nil { - return response.SmartError(err) - } - - // Get the name of the storage volume. - volumeName, err := url.PathUnescape(mux.Vars(r)["volumeName"]) + details, err := request.GetCtxValue[storageVolumeDetails](r.Context(), ctxStorageVolumeDetails) if err != nil { return response.SmartError(err) } @@ -1121,20 +946,13 @@ func storagePoolVolumeSnapshotTypeDelete(d *Daemon, r *http.Request) response.Re return response.SmartError(err) } - // Convert the volume type name to our internal integer representation. - volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) - if err != nil { - return response.BadRequest(err) - } - // Check that the storage volume type is valid. - if volumeType != dbCluster.StoragePoolVolumeTypeCustom { - return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName)) + if details.volumeType != dbCluster.StoragePoolVolumeTypeCustom { + return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", details.volumeTypeName)) } - // Get the project name. requestProjectName := request.ProjectParam(r) - projectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, volumeType) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } @@ -1145,23 +963,19 @@ func storagePoolVolumeSnapshotTypeDelete(d *Daemon, r *http.Request) response.Re return resp } - fullSnapshotName := fmt.Sprintf("%s/%s", volumeName, snapshotName) - resp = forwardedResponseIfVolumeIsRemote(s, r, poolName, projectName, fullSnapshotName, volumeType) + resp = forwardedResponseIfVolumeIsRemote(s, r) if resp != nil { return resp } - snapshotDelete := func(op *operations.Operation) error { - pool, err := storagePools.LoadByName(s, poolName) - if err != nil { - return err - } + fullSnapshotName := fmt.Sprintf("%s/%s", details.volumeName, snapshotName) - return pool.DeleteCustomVolumeSnapshot(projectName, fullSnapshotName, op) + snapshotDelete := func(op *operations.Operation) error { + return details.pool.DeleteCustomVolumeSnapshot(effectiveProjectName, fullSnapshotName, op) } resources := map[string][]api.URL{} - resources["storage_volume_snapshots"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", poolName, "volumes", volumeTypeName, volumeName, "snapshots", snapshotName)} + resources["storage_volume_snapshots"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", details.pool.Name(), "volumes", details.volumeTypeName, details.volumeName, "snapshots", snapshotName)} op, err := operations.OperationCreate(s, requestProjectName, operations.OperationClassTask, operationtype.VolumeSnapshotDelete, resources, nil, snapshotDelete, nil, nil, r) if err != nil { diff --git a/shared/entity/type.go b/shared/entity/type.go index eea8f3ddc53c..cb0db5e18c25 100644 --- a/shared/entity/type.go +++ b/shared/entity/type.go @@ -221,11 +221,8 @@ func (t Type) URL(projectName string, location string, pathArguments ...string) u = u.WithQuery("project", projectName) } - // Always set location if provided. - if location != "" { - u = u.WithQuery("target", location) - } - + // Always set location if provided (empty or "none" locations are ignored). + u = u.Target(location) return u, nil }