incus-mirror/cmd/incusd/storage_volumes.go
Stéphane Graber e2547b834f
incusd/storage: Validate volume name on ISO and backup import
This addresses GHSA-67qw-68v3-36h6 (CVE pending)

Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
2026-07-30 09:45:38 -04:00

3044 lines
90 KiB
Go

package main
import (
"bytes"
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"slices"
"sort"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/lxc/incus/v7/internal/filter"
internalInstance "github.com/lxc/incus/v7/internal/instance"
internalIO "github.com/lxc/incus/v7/internal/io"
"github.com/lxc/incus/v7/internal/server/auth"
"github.com/lxc/incus/v7/internal/server/backup"
"github.com/lxc/incus/v7/internal/server/cluster"
"github.com/lxc/incus/v7/internal/server/db"
dbCluster "github.com/lxc/incus/v7/internal/server/db/cluster"
"github.com/lxc/incus/v7/internal/server/db/operationtype"
"github.com/lxc/incus/v7/internal/server/instance"
"github.com/lxc/incus/v7/internal/server/operations"
"github.com/lxc/incus/v7/internal/server/project"
"github.com/lxc/incus/v7/internal/server/request"
"github.com/lxc/incus/v7/internal/server/response"
"github.com/lxc/incus/v7/internal/server/state"
storagePools "github.com/lxc/incus/v7/internal/server/storage"
storageDrivers "github.com/lxc/incus/v7/internal/server/storage/drivers"
localUtil "github.com/lxc/incus/v7/internal/server/util"
internalUtil "github.com/lxc/incus/v7/internal/util"
"github.com/lxc/incus/v7/internal/version"
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/archive"
"github.com/lxc/incus/v7/shared/logger"
"github.com/lxc/incus/v7/shared/revert"
localtls "github.com/lxc/incus/v7/shared/tls"
"github.com/lxc/incus/v7/shared/util"
"github.com/lxc/incus/v7/shared/validate"
)
var storagePoolVolumesCmd = APIEndpoint{
Path: "storage-pools/{poolName}/volumes",
Get: APIEndpointAction{Handler: storagePoolVolumesGet, AccessHandler: allowAuthenticated},
Post: APIEndpointAction{Handler: storagePoolVolumesPost, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanCreateStorageVolumes), LargeRequest: true},
}
var storagePoolVolumesTypeCmd = APIEndpoint{
Path: "storage-pools/{poolName}/volumes/{type}",
Get: APIEndpointAction{Handler: storagePoolVolumesGet, AccessHandler: allowAuthenticated},
Post: APIEndpointAction{Handler: storagePoolVolumesPost, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanCreateStorageVolumes), LargeRequest: true},
}
var storagePoolVolumeTypeCmd = APIEndpoint{
Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}",
Delete: APIEndpointAction{Handler: storagePoolVolumeDelete, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanEdit, "poolName", "type", "volumeName", "location")},
Get: APIEndpointAction{Handler: storagePoolVolumeGet, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanView, "poolName", "type", "volumeName", "location")},
Patch: APIEndpointAction{Handler: storagePoolVolumePatch, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanEdit, "poolName", "type", "volumeName", "location")},
Post: APIEndpointAction{Handler: storagePoolVolumePost, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanEdit, "poolName", "type", "volumeName", "location")},
Put: APIEndpointAction{Handler: storagePoolVolumePut, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanEdit, "poolName", "type", "volumeName", "location")},
}
var storagePoolVolumeTypeNBDCmd = APIEndpoint{
Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}/nbd",
Get: APIEndpointAction{Handler: storagePoolVolumeTypeNBDHandler, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanConnectNBD, "poolName", "type", "volumeName", "location")},
}
var storagePoolVolumeTypeSFTPCmd = APIEndpoint{
Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}/sftp",
Get: APIEndpointAction{Handler: storagePoolVolumeTypeSFTPHandler, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanConnectSFTP, "poolName", "type", "volumeName", "location")},
}
var storagePoolVolumeTypeFileCmd = APIEndpoint{
Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}/files",
Delete: APIEndpointAction{Handler: storagePoolVolumeTypeFileHandler, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanAccessFiles, "poolName", "type", "volumeName", "location")},
Get: APIEndpointAction{Handler: storagePoolVolumeTypeFileHandler, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanAccessFiles, "poolName", "type", "volumeName", "location")},
Head: APIEndpointAction{Handler: storagePoolVolumeTypeFileHandler, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanAccessFiles, "poolName", "type", "volumeName", "location")},
Post: APIEndpointAction{Handler: storagePoolVolumeTypeFileHandler, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanAccessFiles, "poolName", "type", "volumeName", "location"), LargeRequest: true},
}
// swagger:operation GET /1.0/storage-pools/{poolName}/volumes storage storage_pool_volumes_get
//
// Get the storage volumes
//
// Returns a list of storage volumes (URLs).
//
// ---
// produces:
// - application/json
// parameters:
// - in: path
// name: poolName
// description: Storage pool name
// type: string
// required: true
// - in: query
// name: project
// description: Project name
// type: string
// example: default
// - in: query
// name: target
// description: Cluster member name
// type: string
// example: server01
// - in: query
// name: filter
// description: Collection filter
// type: string
// example: default
// responses:
// "200":
// description: API endpoints
// schema:
// type: object
// description: Sync response
// properties:
// type:
// type: string
// description: Response type
// example: sync
// status:
// type: string
// description: Status description
// example: Success
// status_code:
// type: integer
// description: Status code
// example: 200
// metadata:
// type: array
// description: List of endpoints
// items:
// type: string
// example: |-
// [
// "/1.0/storage-pools/local/volumes/container/a1",
// "/1.0/storage-pools/local/volumes/container/a2",
// "/1.0/storage-pools/local/volumes/custom/backups",
// "/1.0/storage-pools/local/volumes/custom/images"
// ]
// "400":
// $ref: "#/responses/BadRequest"
// "403":
// $ref: "#/responses/Forbidden"
// "404":
// $ref: "#/responses/NotFound"
// "409":
// $ref: "#/responses/Conflict"
// "500":
// $ref: "#/responses/InternalServerError"
// swagger:operation GET /1.0/storage-pools/{poolName}/volumes?recursion=1 storage storage_pool_volumes_get_recursion1
//
// Get the storage volumes
//
// Returns a list of storage volumes (structs).
//
// ---
// produces:
// - application/json
// parameters:
// - in: path
// name: poolName
// description: Storage pool name
// type: string
// required: true
// - in: query
// name: project
// description: Project name
// type: string
// example: default
// - in: query
// name: target
// description: Cluster member name
// type: string
// example: server01
// - in: query
// name: filter
// description: Collection filter
// type: string
// example: default
// responses:
// "200":
// description: API endpoints
// schema:
// type: object
// description: Sync response
// properties:
// type:
// type: string
// description: Response type
// example: sync
// status:
// type: string
// description: Status description
// example: Success
// status_code:
// type: integer
// description: Status code
// example: 200
// metadata:
// type: array
// description: List of storage volumes
// items:
// $ref: "#/definitions/StorageVolume"
// "400":
// $ref: "#/responses/BadRequest"
// "403":
// $ref: "#/responses/Forbidden"
// "404":
// $ref: "#/responses/NotFound"
// "409":
// $ref: "#/responses/Conflict"
// "500":
// $ref: "#/responses/InternalServerError"
// swagger:operation GET /1.0/storage-pools/{poolName}/volumes/{type} storage storage_pool_volumes_type_get
//
// Get the storage volumes
//
// Returns a list of storage volumes (URLs) (type specific endpoint).
//
// ---
// produces:
// - application/json
// parameters:
// - in: path
// name: poolName
// description: Storage pool name
// type: string
// required: true
// - in: path
// name: type
// description: Storage volume type
// type: string
// required: true
// - in: query
// name: project
// description: Project name
// type: string
// example: default
// - in: query
// name: target
// description: Cluster member name
// type: string
// example: server01
// responses:
// "200":
// description: API endpoints
// schema:
// type: object
// description: Sync response
// properties:
// type:
// type: string
// description: Response type
// example: sync
// status:
// type: string
// description: Status description
// example: Success
// status_code:
// type: integer
// description: Status code
// example: 200
// metadata:
// type: array
// description: List of endpoints
// items:
// type: string
// example: |-
// [
// "/1.0/storage-pools/local/volumes/custom/backups",
// "/1.0/storage-pools/local/volumes/custom/images"
// ]
// "400":
// $ref: "#/responses/BadRequest"
// "403":
// $ref: "#/responses/Forbidden"
// "404":
// $ref: "#/responses/NotFound"
// "409":
// $ref: "#/responses/Conflict"
// "500":
// $ref: "#/responses/InternalServerError"
// swagger:operation GET /1.0/storage-pools/{poolName}/volumes/{type}?recursion=1 storage storage_pool_volumes_type_get_recursion1
//
// Get the storage volumes
//
// Returns a list of storage volumes (structs) (type specific endpoint).
//
// ---
// produces:
// - application/json
// parameters:
// - in: path
// name: poolName
// description: Storage pool name
// type: string
// required: true
// - in: path
// name: type
// description: Storage volume type
// type: string
// required: true
// - in: query
// name: project
// description: Project name
// type: string
// example: default
// - in: query
// name: target
// description: Cluster member name
// type: string
// example: server01
// responses:
// "200":
// description: API endpoints
// schema:
// type: object
// description: Sync response
// properties:
// type:
// type: string
// description: Response type
// example: sync
// status:
// type: string
// description: Status description
// example: Success
// status_code:
// type: integer
// description: Status code
// example: 200
// metadata:
// type: array
// description: List of storage volumes
// items:
// $ref: "#/definitions/StorageVolume"
// "400":
// $ref: "#/responses/BadRequest"
// "403":
// $ref: "#/responses/Forbidden"
// "404":
// $ref: "#/responses/NotFound"
// "409":
// $ref: "#/responses/Conflict"
// "500":
// $ref: "#/responses/InternalServerError"
// swagger:operation GET /1.0/storage-pools/{poolName}/volumes/{type}?recursion=2 storage storage_pool_volumes_type_get_recursion2
//
// Get the storage volumes with all details
//
// Returns a list of storage volumes (structs) including all details (type specific endpoint).
//
// ---
// produces:
// - application/json
// parameters:
// - in: path
// name: poolName
// description: Storage pool name
// type: string
// required: true
// - in: path
// name: type
// description: Storage volume type
// type: string
// required: true
// - in: query
// name: project
// description: Project name
// type: string
// example: default
// - in: query
// name: target
// description: Cluster member name
// type: string
// example: server01
// responses:
// "200":
// description: API endpoints
// schema:
// type: object
// description: Sync response
// properties:
// type:
// type: string
// description: Response type
// example: sync
// status:
// type: string
// description: Status description
// example: Success
// status_code:
// type: integer
// description: Status code
// example: 200
// metadata:
// type: array
// description: List of storage volumes
// items:
// $ref: "#/definitions/StorageVolumeFull"
// "400":
// $ref: "#/responses/BadRequest"
// "403":
// $ref: "#/responses/Forbidden"
// "404":
// $ref: "#/responses/NotFound"
// "409":
// $ref: "#/responses/Conflict"
// "500":
// $ref: "#/responses/InternalServerError"
func storagePoolVolumesGet(d *Daemon, r *http.Request) response.Response {
s := d.State()
resp := forwardedResponseIfTargetIsRemote(s, r)
if resp != nil {
return resp
}
targetMember := request.QueryParam(r, "target")
memberSpecific := targetMember != ""
poolName, err := pathVar(r, "poolName")
if err != nil {
return response.SmartError(err)
}
// Get the name of the volume type.
volumeTypeName, err := pathVar(r, "type")
if err != nil {
return response.SmartError(err)
}
// Convert volume type name to internal integer representation if requested.
var volumeType int
if volumeTypeName != "" {
volumeType, err = storagePools.VolumeTypeNameToDBType(volumeTypeName)
if err != nil {
return response.BadRequest(err)
}
}
filterStr := r.FormValue("filter")
clauses, err := filter.Parse(filterStr, filter.QueryOperatorSet())
if err != nil {
return response.SmartError(fmt.Errorf("Invalid filter: %w", err))
}
// Retrieve the storage pool (and check if the storage pool exists).
pool, err := storagePools.LoadByName(s, poolName)
if err != nil {
return response.SmartError(err)
}
// Detect project mode.
requestProjectName := request.QueryParam(r, "project")
allProjects := util.IsTrue(request.QueryParam(r, "all-projects"))
if allProjects && requestProjectName != "" {
return response.SmartError(api.StatusErrorf(http.StatusBadRequest, "Cannot specify a project when requesting all projects"))
} else if !allProjects && requestProjectName == "" {
requestProjectName = api.ProjectDefaultName
}
var dbVolumes []*db.StorageVolume
var projectImages []string
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
var customVolProjectName string
if !allProjects {
dbProject, err := dbCluster.GetProject(ctx, tx.Tx(), requestProjectName)
if err != nil {
return err
}
p, err := dbProject.ToAPI(ctx, tx.Tx())
if err != nil {
return err
}
// The project name used for custom volumes varies based on whether the
// project has the features.storage.volumes feature enabled.
customVolProjectName = project.StorageVolumeProjectFromRecord(p, db.StoragePoolVolumeTypeCustom)
projectImages, err = tx.GetImagesFingerprints(ctx, requestProjectName, false)
if err != nil {
return err
}
}
filters := make([]db.StorageVolumeFilter, 0)
for i := range supportedVolumeTypes {
supportedVolType := supportedVolumeTypes[i] // Local variable for use as pointer below.
if volumeTypeName != "" && supportedVolType != volumeType {
continue // Only include the requested type if specified.
}
switch supportedVolType {
case db.StoragePoolVolumeTypeCustom:
volTypeCustom := db.StoragePoolVolumeTypeCustom
filter := db.StorageVolumeFilter{
Type: &volTypeCustom,
}
if !allProjects {
filter.Project = &customVolProjectName
}
filters = append(filters, filter)
case db.StoragePoolVolumeTypeImage:
// Image volumes are effectively a cache and are always linked to default project.
// We filter the ones relevant to requested project below after the query has run.
volTypeImage := db.StoragePoolVolumeTypeImage
filters = append(filters, db.StorageVolumeFilter{
Type: &volTypeImage,
})
default:
// Include instance volume types using the specified project.
filter := db.StorageVolumeFilter{
Type: &supportedVolType,
}
if !allProjects {
filter.Project = &requestProjectName
}
filters = append(filters, filter)
}
}
dbVolumes, err = tx.GetStoragePoolVolumes(ctx, pool.ID(), memberSpecific, filters...)
if err != nil {
return fmt.Errorf("Failed loading storage volumes: %w", err)
}
return err
})
if err != nil {
return response.SmartError(err)
}
// Pre-fill UsedBy if using filtering.
if clauses != nil && len(clauses.Clauses) > 0 {
for i, vol := range dbVolumes {
volumeUsedBy, err := storagePoolVolumeUsedByGet(s, requestProjectName, poolName, vol)
if err != nil {
return response.InternalError(err)
}
dbVolumes[i].UsedBy = project.FilterUsedBy(s.Authorizer, r, volumeUsedBy)
}
}
// Filter the results.
dbVolumes, err = filterVolumes(dbVolumes, clauses, allProjects, projectImages)
if err != nil {
return response.SmartError(err)
}
// Sort by type then volume name.
sort.SliceStable(dbVolumes, func(i, j int) bool {
volA := dbVolumes[i]
volB := dbVolumes[j]
if volA.Type != volB.Type {
return dbVolumes[i].Type < dbVolumes[j].Type
}
return volA.Name < volB.Name
})
userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeStorageVolume)
if err != nil {
return response.SmartError(err)
}
recursionStr := r.FormValue("recursion")
recursion, err := strconv.Atoi(recursionStr)
if err != nil {
recursion = 0
}
if recursion > 0 {
volumes := make([]*api.StorageVolume, 0, len(dbVolumes))
for _, dbVol := range dbVolumes {
vol := &dbVol.StorageVolume
var location string
if s.ServerClustered && !pool.Driver().Info().Remote {
location = vol.Location
}
volumeName, snapName, _ := api.GetParentAndSnapshotName(vol.Name)
if snapName != "" {
continue
}
if !userHasPermission(auth.ObjectStorageVolume(vol.Project, poolName, dbVol.Type, volumeName, location)) {
continue
}
// Fill in UsedBy if we haven't previously done so.
if clauses == nil || len(clauses.Clauses) == 0 {
volumeUsedBy, err := storagePoolVolumeUsedByGet(s, requestProjectName, poolName, dbVol)
if err != nil {
return response.InternalError(err)
}
vol.UsedBy = project.FilterUsedBy(s.Authorizer, r, volumeUsedBy)
}
volumes = append(volumes, vol)
}
if recursion == 2 {
volumesFull := make([]*api.StorageVolumeFull, 0, len(volumes))
for _, vol := range volumes {
if s.ServerClustered && !pool.Driver().Info().Remote && vol.Location != "" && vol.Location != s.ServerName {
// Get the remote address.
var volNode db.NodeInfo
err := s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
volNode, err = tx.GetNodeByName(ctx, vol.Location)
return err
})
if err != nil {
return response.InternalError(fmt.Errorf("Failed getting cluster member info for %q: %w", vol.Location, err))
}
client, err := cluster.Connect(volNode.Address, s.Endpoints.NetworkCert(), s.ServerCert(), r, false)
if err != nil {
return response.InternalError(err)
}
fullVol, _, err := client.UseTarget(vol.Location).UseProject(vol.Project).GetStoragePoolVolumeFull(poolName, vol.Type, vol.Name)
if err != nil {
return response.InternalError(err)
}
volumesFull = append(volumesFull, fullVol)
continue
}
// Handle local volumes.
fullVol, err := getVolumeFull(r.Context(), s, poolName, *vol)
if err != nil {
return response.InternalError(err)
}
volumesFull = append(volumesFull, fullVol)
}
return response.SyncResponse(true, volumesFull)
}
return response.SyncResponse(true, volumes)
}
urls := make([]string, 0, len(dbVolumes))
for _, dbVol := range dbVolumes {
volumeName, snapName, _ := api.GetParentAndSnapshotName(dbVol.Name)
if snapName != "" {
continue
}
var location string
if s.ServerClustered && !pool.Driver().Info().Remote {
location = dbVol.Location
}
if !userHasPermission(auth.ObjectStorageVolume(dbVol.Project, poolName, dbVol.Type, volumeName, location)) {
continue
}
urls = append(urls, dbVol.StorageVolume.URL(version.APIVersion, poolName).String())
}
return response.SyncResponse(true, urls)
}
// filterVolumes returns a filtered list of volumes that match the given clauses.
func filterVolumes(volumes []*db.StorageVolume, clauses *filter.ClauseSet, allProjects bool, filterProjectImages []string) ([]*db.StorageVolume, error) {
// FilterStorageVolume is for filtering purpose only.
// It allows to filter snapshots by using default filter mechanism.
type FilterStorageVolume struct {
api.StorageVolume `yaml:",inline"`
Snapshot string `yaml:"snapshot"`
}
filtered := []*db.StorageVolume{}
for _, volume := range volumes {
// Filter out image volumes that are not used by this project.
if volume.Type == db.StoragePoolVolumeTypeNameImage && !allProjects && !slices.Contains(filterProjectImages, volume.Name) {
continue
}
tmpVolume := FilterStorageVolume{
StorageVolume: volume.StorageVolume,
Snapshot: strconv.FormatBool(strings.Contains(volume.Name, internalInstance.SnapshotDelimiter)),
}
match, err := filter.Match(tmpVolume, *clauses)
if err != nil {
return nil, err
}
if !match {
continue
}
filtered = append(filtered, volume)
}
return filtered, nil
}
// swagger:operation POST /1.0/storage-pools/{poolName}/volumes storage storage_pool_volumes_post
//
// Add a storage volume
//
// Creates a new storage volume.
// Will return an empty sync response on simple volume creation but an operation on copy or migration.
//
// ---
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - in: path
// name: poolName
// description: Storage pool name
// type: string
// required: true
// - in: query
// name: project
// description: Project name
// type: string
// example: default
// - in: query
// name: target
// description: Cluster member name
// type: string
// example: server01
// - in: body
// name: volume
// description: Storage volume
// required: true
// schema:
// $ref: "#/definitions/StorageVolumesPost"
// responses:
// "200":
// $ref: "#/responses/EmptySyncResponse"
// "202":
// $ref: "#/responses/Operation"
// "400":
// $ref: "#/responses/BadRequest"
// "403":
// $ref: "#/responses/Forbidden"
// "404":
// $ref: "#/responses/NotFound"
// "409":
// $ref: "#/responses/Conflict"
// "500":
// $ref: "#/responses/InternalServerError"
// swagger:operation POST /1.0/storage-pools/{poolName}/volumes/{type} storage storage_pool_volumes_type_post
//
// Add a storage volume
//
// Creates a new storage volume (type specific endpoint).
// Will return an empty sync response on simple volume creation but an operation on copy or migration.
//
// ---
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - in: path
// name: poolName
// description: Storage pool name
// type: string
// required: true
// - in: path
// name: type
// description: Storage volume type
// type: string
// required: true
// - in: query
// name: project
// description: Project name
// type: string
// example: default
// - in: query
// name: target
// description: Cluster member name
// type: string
// example: server01
// - in: body
// name: volume
// description: Storage volume
// required: true
// schema:
// $ref: "#/definitions/StorageVolumesPost"
// responses:
// "200":
// $ref: "#/responses/EmptySyncResponse"
// "202":
// $ref: "#/responses/Operation"
// "400":
// $ref: "#/responses/BadRequest"
// "403":
// $ref: "#/responses/Forbidden"
// "404":
// $ref: "#/responses/NotFound"
// "409":
// $ref: "#/responses/Conflict"
// "500":
// $ref: "#/responses/InternalServerError"
func storagePoolVolumesPost(d *Daemon, r *http.Request) response.Response {
s := d.State()
poolName, err := pathVar(r, "poolName")
if err != nil {
return response.SmartError(err)
}
projectName, err := project.StorageVolumeProject(s.DB.Cluster, request.ProjectParam(r), db.StoragePoolVolumeTypeCustom)
if err != nil {
return response.SmartError(err)
}
resp := forwardedResponseIfTargetIsRemote(s, r)
if resp != nil {
return resp
}
// If we're getting binary content, process separately.
if r.Header.Get("Content-Type") == "application/octet-stream" {
if r.Header.Get("X-Incus-type") == "iso" {
return createStoragePoolVolumeFromISO(s, r, request.ProjectParam(r), projectName, r.Body, poolName, r.Header.Get("X-Incus-name"))
}
return createStoragePoolVolumeFromBackup(s, r, request.ProjectParam(r), projectName, r.Body, poolName, r.Header.Get("X-Incus-name"))
}
req := api.StorageVolumesPost{}
// Parse the request.
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
return response.BadRequest(err)
}
// Quick checks.
err = validate.IsAPIName(req.Name, false)
if err != nil {
return response.BadRequest(fmt.Errorf("Invalid storage volume name: %w", err))
}
// Backward compatibility.
if req.ContentType == "" {
req.ContentType = db.StoragePoolVolumeContentTypeNameFS
}
_, err = storagePools.VolumeContentTypeNameToContentType(req.ContentType)
if err != nil {
return response.BadRequest(err)
}
// Handle being called through the typed URL.
if r.PathValue("type") != "" {
req.Type, err = pathVar(r, "type")
if err != nil {
return response.SmartError(err)
}
}
// We currently only allow to create storage volumes of type storagePoolVolumeTypeCustom.
// So check, that nothing else was requested.
if req.Type != db.StoragePoolVolumeTypeNameCustom {
return response.BadRequest(fmt.Errorf("Currently not allowed to create storage volumes of type %q", req.Type))
}
var poolID int64
var dbVolume *db.StorageVolume
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
poolID, err = tx.GetStoragePoolID(ctx, poolName)
if err != nil {
return err
}
// Check if destination volume exists.
dbVolume, err = tx.GetStoragePoolVolume(ctx, poolID, projectName, db.StoragePoolVolumeTypeCustom, req.Name, true)
if err != nil && !response.IsNotFoundError(err) {
return err
}
err = project.AllowVolumeCreation(tx, projectName, poolName, req)
if err != nil {
return err
}
return nil
})
if err != nil {
return response.SmartError(err)
} else if dbVolume != nil && !req.Source.Refresh {
return response.Conflict(errors.New("Volume by that name already exists"))
}
// Check if we need to switch to migration
serverName := s.ServerName
var nodeAddress string
if s.ServerClustered && (req.Source.Location != "" && serverName != req.Source.Location) {
err := s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
nodeInfo, err := tx.GetNodeByName(ctx, req.Source.Location)
if err != nil {
return err
}
nodeAddress = nodeInfo.Address
return nil
})
if err != nil {
return response.SmartError(err)
}
if nodeAddress == "" {
return response.BadRequest(errors.New("The source is currently offline"))
}
return clusterCopyCustomVolumeInternal(s, r, nodeAddress, projectName, poolName, &req)
}
switch req.Source.Type {
case "":
err = validateCreateConfig(req.Config)
if err != nil {
return response.SmartError(err)
}
return doVolumeCreateOrCopy(s, r, request.ProjectParam(r), projectName, poolName, &req)
case "copy":
// Check that the caller is allowed to view the source volume.
srcProjectName := projectName
if req.Source.Project != "" {
srcProjectName, err = project.StorageVolumeProject(s.DB.Cluster, req.Source.Project, db.StoragePoolVolumeTypeCustom)
if err != nil {
return response.SmartError(err)
}
}
srcPoolName := req.Source.Pool
if srcPoolName == "" {
srcPoolName = poolName
}
err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectStorageVolume(srcProjectName, srcPoolName, db.StoragePoolVolumeTypeNameCustom, req.Source.Name, req.Source.Location), auth.EntitlementCanView)
if err != nil {
return response.SmartError(err)
}
if dbVolume != nil {
return doCustomVolumeRefresh(s, r, request.ProjectParam(r), projectName, poolName, &req)
}
return doVolumeCreateOrCopy(s, r, request.ProjectParam(r), projectName, poolName, &req)
case "migration":
return doVolumeMigration(s, r, request.ProjectParam(r), projectName, poolName, &req)
default:
return response.BadRequest(fmt.Errorf("Unknown source type %q", req.Source.Type))
}
}
// validateCreateConfig validates the configuration at creation time
// and rejects keys that are not allowed when creating the storage volume.
func validateCreateConfig(config map[string]string) error {
if util.IsTrue(config["dependent"]) {
return errors.New("Config key 'dependent' cannot be set on creation")
}
return nil
}
func clusterCopyCustomVolumeInternal(s *state.State, r *http.Request, sourceAddress string, projectName string, poolName string, req *api.StorageVolumesPost) response.Response {
websockets := map[string]string{}
client, err := cluster.Connect(sourceAddress, s.Endpoints.NetworkCert(), s.ServerCert(), r, false)
if err != nil {
return response.SmartError(err)
}
sourceProject := projectName
if req.Source.Project != "" {
sourceProject = req.Source.Project
}
client = client.UseProject(sourceProject)
pullReq := api.StorageVolumePost{
Name: req.Source.Name,
Pool: req.Source.Pool,
Migration: true,
VolumeOnly: req.Source.VolumeOnly,
Source: api.StorageVolumeSource{
Location: req.Source.Location,
},
}
if sourceProject != projectName {
pullReq.Project = projectName
}
op, err := client.MigrateStoragePoolVolume(req.Source.Pool, pullReq)
if err != nil {
return response.SmartError(err)
}
opAPI := op.Get()
for k, v := range opAPI.Metadata {
vStr, ok := v.(string)
if !ok {
continue
}
websockets[k] = vStr
}
// Reset the source for a migration
req.Source.Type = "migration"
req.Source.Certificate = string(s.Endpoints.NetworkCert().PublicKey())
req.Source.Mode = "pull"
req.Source.Operation = fmt.Sprintf("https://%s/%s/operations/%s", sourceAddress, version.APIVersion, opAPI.ID)
req.Source.Websockets = websockets
req.Source.Project = ""
return doVolumeMigration(s, r, req.Source.Project, projectName, poolName, req)
}
func doCustomVolumeRefresh(s *state.State, r *http.Request, requestProjectName string, projectName string, poolName string, req *api.StorageVolumesPost) response.Response {
pool, err := storagePools.LoadByName(s, poolName)
if err != nil {
return response.SmartError(err)
}
var srcProjectName string
if req.Source.Project != "" {
srcProjectName, err = project.StorageVolumeProject(s.DB.Cluster, req.Source.Project, db.StoragePoolVolumeTypeCustom)
if err != nil {
return response.SmartError(err)
}
}
run := func(op *operations.Operation) error {
reverter := revert.New()
defer reverter.Fail()
if req.Source.Name == "" {
return errors.New("No source volume name supplied")
}
err = pool.RefreshCustomVolume(projectName, srcProjectName, req.Name, req.Description, req.Config, req.Source.Pool, req.Source.Name, !req.Source.VolumeOnly, req.Source.RefreshExcludeOlder, op)
if err != nil {
return err
}
reverter.Success()
return nil
}
op, err := operations.OperationCreate(s, requestProjectName, operations.OperationClassTask, operationtype.VolumeCopy, nil, nil, run, nil, nil, r)
if err != nil {
return response.InternalError(err)
}
return operations.OperationResponse(op)
}
func doVolumeCreateOrCopy(s *state.State, r *http.Request, requestProjectName string, projectName string, poolName string, req *api.StorageVolumesPost) response.Response {
pool, err := storagePools.LoadByName(s, poolName)
if err != nil {
return response.SmartError(err)
}
var srcProjectName string
if req.Source.Project != "" {
srcProjectName, err = project.StorageVolumeProject(s.DB.Cluster, req.Source.Project, db.StoragePoolVolumeTypeCustom)
if err != nil {
return response.SmartError(err)
}
}
volumeDBContentType, err := storagePools.VolumeContentTypeNameToContentType(req.ContentType)
if err != nil {
return response.SmartError(err)
}
contentType, err := storagePools.VolumeDBContentTypeToContentType(volumeDBContentType)
if err != nil {
return response.SmartError(err)
}
run := func(op *operations.Operation) error {
if req.Source.Name == "" {
// Use an empty operation for this sync response to pass the requestor
op := &operations.Operation{}
op.SetRequestor(r)
return pool.CreateCustomVolume(projectName, req.Name, req.Description, req.Config, contentType, op)
}
return pool.CreateCustomVolumeFromCopy(projectName, srcProjectName, req.Name, req.Description, req.Config, req.Source.Pool, req.Source.Name, !req.Source.VolumeOnly, op)
}
// If no source name supplied then this a volume create operation.
if req.Source.Name == "" {
err := run(nil)
if err != nil {
return response.SmartError(err)
}
return response.EmptySyncResponse
}
// Volume copy operations potentially take a long time, so run as an async operation.
op, err := operations.OperationCreate(s, requestProjectName, operations.OperationClassTask, operationtype.VolumeCopy, nil, nil, run, nil, nil, r)
if err != nil {
return response.InternalError(err)
}
return operations.OperationResponse(op)
}
func doVolumeMigration(s *state.State, r *http.Request, requestProjectName string, projectName string, poolName string, req *api.StorageVolumesPost) response.Response {
// Validate migration mode
if req.Source.Mode != "pull" && req.Source.Mode != "push" {
return response.NotImplemented(fmt.Errorf("Mode '%s' not implemented", req.Source.Mode))
}
// create new certificate
var err error
var cert *x509.Certificate
if req.Source.Certificate != "" {
certBlock, _ := pem.Decode([]byte(req.Source.Certificate))
if certBlock == nil {
return response.InternalError(errors.New("Invalid certificate"))
}
cert, err = x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return response.InternalError(err)
}
}
config, err := localtls.GetTLSConfig(cert)
if err != nil {
return response.InternalError(err)
}
push := req.Source.Mode == "push"
// Initialize migrationArgs, don't set the Storage property yet, this is done in DoStorage,
// to avoid this function relying on the legacy storage layer.
migrationArgs := migrationSinkArgs{
URL: req.Source.Operation,
Dialer: &websocket.Dialer{
TLSClientConfig: config,
NetDialContext: localtls.RFC3493Dialer,
HandshakeTimeout: time.Second * 5,
},
Secrets: req.Source.Websockets,
Push: push,
VolumeOnly: req.Source.VolumeOnly,
Refresh: req.Source.Refresh,
RefreshExcludeOlder: req.Source.RefreshExcludeOlder,
}
sink, err := newStorageMigrationSink(&migrationArgs)
if err != nil {
return response.InternalError(err)
}
resources := map[string][]api.URL{}
resources["storage_volumes"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", poolName, "volumes", "custom", req.Name)}
run := func(op *operations.Operation) error {
// And finally run the migration.
err = sink.DoStorage(s, projectName, poolName, req, op)
if err != nil {
logger.Error("Error during migration sink", logger.Ctx{"err": err})
return fmt.Errorf("Error transferring storage volume: %s", err)
}
return nil
}
var op *operations.Operation
if push {
op, err = operations.OperationCreate(s, requestProjectName, operations.OperationClassWebsocket, operationtype.VolumeCreate, resources, sink.Metadata(), run, sink.Cancel, sink.Connect, r)
if err != nil {
return response.InternalError(err)
}
} else {
op, err = operations.OperationCreate(s, requestProjectName, operations.OperationClassTask, operationtype.VolumeCopy, resources, nil, run, nil, nil, r)
if err != nil {
return response.InternalError(err)
}
}
return operations.OperationResponse(op)
}
// swagger:operation POST /1.0/storage-pools/{poolName}/volumes/{type}/{volumeName} storage storage_pool_volume_type_post
//
// Rename or move/migrate a storage volume
//
// Renames, moves a storage volume between pools or migrates an instance to another server.
//
// The returned operation metadata will vary based on what's requested.
// For rename or move within the same server, this is a simple background operation with progress data.
// For migration, in the push case, this will similarly be a background
// operation with progress data, for the pull case, it will be a websocket
// operation with a number of secrets to be passed to the target server.
//
// ---
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - in: path
// name: poolName
// description: Storage pool name
// type: string
// required: true
// - in: path
// name: type
// description: Storage volume type
// type: string
// required: true
// - in: path
// name: volumeName
// description: Storage volume name
// type: string
// required: true
// - in: query
// name: project
// description: Project name
// type: string
// example: default
// - in: query
// name: target
// description: Cluster member name
// type: string
// example: server01
// - in: body
// name: migration
// description: Migration request
// schema:
// $ref: "#/definitions/StorageVolumePost"
// responses:
// "201":
// $ref: "#/responses/EmptySyncResponse"
// "202":
// $ref: "#/responses/Operation"
// "400":
// $ref: "#/responses/BadRequest"
// "403":
// $ref: "#/responses/Forbidden"
// "404":
// $ref: "#/responses/NotFound"
// "409":
// $ref: "#/responses/Conflict"
// "500":
// $ref: "#/responses/InternalServerError"
func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response {
s := d.State()
// Get the name of the storage volume.
volumeName, err := pathVar(r, "volumeName")
if err != nil {
return response.SmartError(err)
}
volumeTypeName, err := pathVar(r, "type")
if err != nil {
return response.SmartError(err)
}
if internalInstance.IsSnapshot(volumeName) {
return response.BadRequest(errors.New("Invalid volume name"))
}
// Get the name of the storage pool the volume is supposed to be attached to.
srcPoolName, err := pathVar(r, "poolName")
if err != nil {
return response.SmartError(err)
}
req := api.StorageVolumePost{}
// Parse the request.
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
return response.BadRequest(err)
}
// Quick checks.
err = validate.IsAPIName(req.Name, false)
if err != nil {
return response.BadRequest(fmt.Errorf("Invalid storage volume name: %w", err))
}
// Check requested new volume name is not a snapshot volume.
if internalInstance.IsSnapshot(req.Name) {
return response.BadRequest(errors.New("Storage volume names may not contain slashes"))
}
// We currently only allow to create storage volumes of type storagePoolVolumeTypeCustom.
// So check, that nothing else was requested.
if volumeTypeName != db.StoragePoolVolumeTypeNameCustom {
return response.BadRequest(fmt.Errorf("Renaming storage volumes of type %q is not allowed", volumeTypeName))
}
projectName, err := project.StorageVolumeProject(s.DB.Cluster, request.ProjectParam(r), db.StoragePoolVolumeTypeCustom)
if err != nil {
return response.SmartError(err)
}
targetProjectName := projectName
if req.Project != "" {
targetProjectName, err = project.StorageVolumeProject(s.DB.Cluster, req.Project, db.StoragePoolVolumeTypeCustom)
if err != nil {
return response.SmartError(err)
}
// Check whether the effective storage project differs from the requested target project.
// If they do it means that the requested target project doesn't have features.storage.volumes
// and this means that the volume would effectively be moved into the default project, and so we
// require the user explicitly indicates this by targeting it directly.
if targetProjectName != req.Project {
return response.BadRequest(errors.New("Target project does not have features.storage.volumes enabled"))
}
if projectName == targetProjectName {
return response.BadRequest(errors.New("Project and target project are the same"))
}
// Check if user has access to effective storage target project
err := s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(targetProjectName), auth.EntitlementCanCreateStorageVolumes)
if err != nil {
return response.SmartError(err)
}
}
// We need to restore the body of the request since it has already been read, and if we
// forwarded it now no body would be written out.
buf := bytes.Buffer{}
err = json.NewEncoder(&buf).Encode(req)
if err != nil {
return response.SmartError(err)
}
r.Body = internalIO.BytesReadCloser{Buf: &buf}
target := request.QueryParam(r, "target")
// Check if clustered.
if s.ServerClustered && target != "" && req.Source.Location != "" && req.Migration {
var sourceNodeOffline bool
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
// Load source node.
nodeInfo, err := tx.GetNodeByName(ctx, req.Source.Location)
if err != nil {
return err
}
sourceAddress := nodeInfo.Address
if sourceAddress == "" {
// Local node.
sourceNodeOffline = false
return nil
}
sourceMemberInfo, err := tx.GetNodeByAddress(ctx, sourceAddress)
if err != nil {
return fmt.Errorf("Failed to get source member for %q: %w", sourceAddress, err)
}
sourceNodeOffline = sourceMemberInfo.IsOffline(s.GlobalConfig.OfflineThreshold())
return nil
})
if err != nil {
return response.SmartError(err)
}
var targetProject *api.Project
var targetMemberInfo *db.NodeInfo
if sourceNodeOffline {
resp := forwardedResponseIfTargetIsRemote(s, r)
if resp != nil {
return resp
}
srcPool, err := storagePools.LoadByName(s, srcPoolName)
if err != nil {
return response.SmartError(err)
}
if srcPool.Driver().Info().Remote {
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, db.StoragePoolVolumeTypeCustom, 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)
targetIsSet = r.URL.Query().Get("target") != ""
return err
}
return nil
})
if err != nil {
if s.ServerClustered && targetIsSet && volumeNotFound {
return response.NotFound(errors.New("Storage volume not found on this cluster member"))
}
return response.SmartError(err)
}
req := api.StorageVolumePost{
Name: req.Name,
}
return storagePoolVolumeTypePostRename(s, r, srcPool.Name(), projectName, &dbVolume.StorageVolume, req)
}
} else {
resp := forwardedResponseToNode(s, r, req.Source.Location)
if resp != nil {
return resp
}
}
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
p, err := dbCluster.GetProject(ctx, tx.Tx(), projectName)
if err != nil {
return err
}
targetProject, err = p.ToAPI(ctx, tx.Tx())
if err != nil {
return err
}
allMembers, err := tx.GetNodes(ctx)
if err != nil {
return fmt.Errorf("Failed getting cluster members: %w", err)
}
targetMemberInfo, _, err = project.CheckTarget(ctx, s.Authorizer, r, tx, targetProject, target, allMembers)
if err != nil {
return err
}
if targetMemberInfo == nil {
return fmt.Errorf("Failed checking cluster member %q", target)
}
return nil
})
if err != nil {
return response.SmartError(err)
}
if targetMemberInfo.IsOffline(s.GlobalConfig.OfflineThreshold()) {
return response.BadRequest(errors.New("Target cluster member is offline"))
}
run := func(op *operations.Operation) error {
return migrateStorageVolume(s, r, volumeName, srcPoolName, 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)}
op, err := operations.OperationCreate(s, projectName, operations.OperationClassTask, operationtype.VolumeMigrate, resources, nil, run, nil, nil, r)
if err != nil {
return response.InternalError(err)
}
return operations.OperationResponse(op)
}
resp := forwardedResponseIfTargetIsRemote(s, r)
if resp != nil {
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)
if resp != nil {
return resp
}
}
// This is a migration request so send back requested secrets.
if req.Migration {
return storagePoolVolumeTypePostMigration(s, r, request.ProjectParam(r), projectName, srcPoolName, volumeName, req)
}
// Retrieve ID of the storage pool (and check if the storage pool exists).
var targetPoolID int64
var targetPoolName string
if req.Pool != "" {
targetPoolName = req.Pool
} else {
targetPoolName = srcPoolName
}
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
targetPoolID, err = tx.GetStoragePoolID(ctx, targetPoolName)
return err
})
if err != nil {
return response.SmartError(err)
}
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)
return err
})
if !response.IsNotFoundError(err) {
if err != nil {
return response.InternalError(err)
}
return response.Conflict(errors.New("Volume by that name already exists"))
}
// Check if the daemon itself is using it.
frag, err := storagePools.VolumeUsedByDaemon(s, srcPoolName, volumeName)
if err != nil {
return response.SmartError(err)
}
if frag != "" {
return response.SmartError(errors.New("Volume is used by Incus itself and cannot be renamed"))
}
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, volumeType, 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)
targetIsSet = r.URL.Query().Get("target") != ""
return err
}
return nil
})
if err != nil {
if s.ServerClustered && targetIsSet && volumeNotFound {
return response.NotFound(errors.New("Storage volume not found on this cluster member"))
}
return response.SmartError(err)
}
// 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 {
inst, err := instance.Load(s, dbInst, project)
if err != nil {
return err
}
if inst.IsRunning() {
return errors.New("Volume is still in use by running instances")
}
return nil
})
if err != nil {
return response.SmartError(err)
}
// Detect a rename request.
if (req.Pool == "" || req.Pool == srcPoolName) && (projectName == targetProjectName) {
return storagePoolVolumeTypePostRename(s, r, srcPoolName, projectName, &dbVolume.StorageVolume, req)
}
// Otherwise this is a move request.
return storagePoolVolumeTypePostMove(s, r, srcPoolName, projectName, 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 {
if targetNode == req.Source.Location {
return errors.New("Target must be different than storage volumes' current location")
}
var err error
var srcMember, newMember db.NodeInfo
// If the source member is online then get its address so we can connect to it and see if the
// instance is running later.
err = s.DB.Cluster.Transaction(s.ShutdownCtx, func(ctx context.Context, tx *db.ClusterTx) error {
srcMember, err = tx.GetNodeByName(ctx, req.Source.Location)
if err != nil {
return fmt.Errorf("Failed getting current cluster member of storage volume %q", req.Source.Name)
}
newMember, err = tx.GetNodeByName(ctx, targetNode)
if err != nil {
return fmt.Errorf("Failed loading new cluster member for storage volume: %w", err)
}
return nil
})
if err != nil {
return err
}
srcPool, err := storagePools.LoadByName(s, sourcePoolName)
if err != nil {
return fmt.Errorf("Failed loading storage volume storage pool: %w", err)
}
f, err := storageVolumePostClusteringMigrate(s, r, srcPool, projectName, sourceVolumeName, req.Pool, req.Project, req.Name, srcMember, newMember, req.VolumeOnly)
if err != nil {
return err
}
return f(op)
}
func storageVolumePostClusteringMigrate(s *state.State, r *http.Request, srcPool storagePools.Pool, srcProjectName string, srcVolumeName string, newPoolName string, newProjectName string, newVolumeName string, srcMember db.NodeInfo, newMember db.NodeInfo, volumeOnly bool) (func(op *operations.Operation) error, error) {
srcMemberOffline := srcMember.IsOffline(s.GlobalConfig.OfflineThreshold())
// Make sure that the source member is online if we end up being called from another member after a
// redirection due to the source member being offline.
if srcMemberOffline {
return nil, errors.New("The cluster member hosting the storage volume is offline")
}
run := func(op *operations.Operation) error {
if newVolumeName == "" {
newVolumeName = srcVolumeName
}
networkCert := s.Endpoints.NetworkCert()
// Connect to the destination member, i.e. the member to migrate the custom volume to.
// Use the notify argument to indicate to the destination that we are moving a custom volume between
// cluster members.
dest, err := cluster.Connect(newMember.Address, networkCert, s.ServerCert(), r, true)
if err != nil {
return fmt.Errorf("Failed to connect to destination server %q: %w", newMember.Address, err)
}
dest = dest.UseTarget(newMember.Name).UseProject(srcProjectName)
resources := map[string][]api.URL{}
resources["storage_volumes"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", srcPool.Name(), "volumes", "custom", srcVolumeName)}
srcMigration, err := newStorageMigrationSource(volumeOnly, nil)
if err != nil {
return fmt.Errorf("Failed setting up storage volume migration on source: %w", err)
}
run := func(op *operations.Operation) error {
err := srcMigration.DoStorage(s, srcProjectName, srcPool.Name(), srcVolumeName, op)
if err != nil {
return err
}
err = srcPool.DeleteCustomVolume(srcProjectName, srcVolumeName, op)
if err != nil {
return err
}
return nil
}
cancel := func(op *operations.Operation) error {
srcMigration.disconnect()
return nil
}
srcOp, err := operations.OperationCreate(s, srcProjectName, operations.OperationClassWebsocket, operationtype.VolumeMigrate, resources, srcMigration.Metadata(), run, cancel, srcMigration.Connect, r)
if err != nil {
return err
}
err = srcOp.Start()
if err != nil {
return fmt.Errorf("Failed starting migration source operation: %w", err)
}
sourceSecrets := make(map[string]string, len(srcMigration.conns))
for connName, conn := range srcMigration.conns {
sourceSecrets[connName] = conn.Secret()
}
// Request pull mode migration on destination.
err = dest.CreateStoragePoolVolume(newPoolName, api.StorageVolumesPost{
Name: newVolumeName,
Type: "custom",
Source: api.StorageVolumeSource{
Type: "migration",
Mode: "pull",
Operation: fmt.Sprintf("https://%s%s", srcMember.Address, srcOp.URL()),
Websockets: sourceSecrets,
Certificate: string(networkCert.PublicKey()),
Name: newVolumeName,
Pool: newPoolName,
Project: newProjectName,
},
})
if err != nil {
return fmt.Errorf("Failed requesting volume create on destination: %w", err)
}
return nil
}
return run, nil
}
// storagePoolVolumeTypePostMigration handles volume migration type POST requests.
func storagePoolVolumeTypePostMigration(s *state.State, r *http.Request, requestProjectName string, projectName string, poolName string, volumeName string, req api.StorageVolumePost) response.Response {
ws, err := newStorageMigrationSource(req.VolumeOnly, req.Target)
if err != nil {
return response.InternalError(err)
}
resources := map[string][]api.URL{}
srcVolParentName, srcVolSnapName, srcIsSnapshot := api.GetParentAndSnapshotName(volumeName)
if srcIsSnapshot {
resources["storage_volume_snapshots"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", poolName, "volumes", "custom", srcVolParentName, "snapshots", srcVolSnapName)}
} else {
resources["storage_volumes"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", poolName, "volumes", "custom", volumeName)}
}
run := func(op *operations.Operation) error {
return ws.DoStorage(s, projectName, poolName, volumeName, op)
}
if req.Target != nil {
// Push mode.
op, err := operations.OperationCreate(s, requestProjectName, operations.OperationClassTask, operationtype.VolumeMigrate, resources, nil, run, nil, nil, r)
if err != nil {
return response.InternalError(err)
}
return operations.OperationResponse(op)
}
// Pull mode.
op, err := operations.OperationCreate(s, requestProjectName, operations.OperationClassWebsocket, operationtype.VolumeMigrate, resources, ws.Metadata(), run, ws.Cancel, ws.Connect, r)
if err != nil {
return response.InternalError(err)
}
return operations.OperationResponse(op)
}
// storagePoolVolumeTypePostRename handles volume rename type POST requests.
func storagePoolVolumeTypePostRename(s *state.State, r *http.Request, poolName string, projectName string, vol *api.StorageVolume, req api.StorageVolumePost) response.Response {
newVol := *vol
newVol.Name = req.Name
pool, err := storagePools.LoadByName(s, poolName)
if err != nil {
return response.SmartError(err)
}
reverter := revert.New()
defer reverter.Fail()
// Update devices using the volume in instances and profiles.
err = storagePoolVolumeUpdateUsers(r.Context(), s, projectName, pool.Name(), vol, pool.Name(), &newVol)
if err != nil {
return response.SmartError(err)
}
// Use an empty operation for this sync response to pass the requestor
op := &operations.Operation{}
op.SetRequestor(r)
err = pool.RenameCustomVolume(projectName, vol.Name, req.Name, op)
if err != nil {
return response.SmartError(err)
}
reverter.Success()
u := api.NewURL().Path(version.APIVersion, "storage-pools", pool.Name(), "volumes", db.StoragePoolVolumeTypeNameCustom, req.Name).Project(projectName)
return response.SyncResponseLocation(true, nil, u.String())
}
// storagePoolVolumeTypePostMove handles volume move type POST requests.
func storagePoolVolumeTypePostMove(s *state.State, r *http.Request, poolName string, requestProjectName string, projectName string, vol *api.StorageVolume, req api.StorageVolumePost) response.Response {
newVol := *vol
newVol.Name = req.Name
pool, err := storagePools.LoadByName(s, poolName)
if err != nil {
return response.SmartError(err)
}
newPool, err := storagePools.LoadByName(s, req.Pool)
if err != nil {
return response.SmartError(err)
}
run := func(op *operations.Operation) error {
reverter := revert.New()
defer reverter.Fail()
// Update devices using the volume in instances and profiles.
err = storagePoolVolumeUpdateUsers(context.TODO(), s, requestProjectName, pool.Name(), vol, newPool.Name(), &newVol)
if err != nil {
return err
}
reverter.Add(func() {
_ = storagePoolVolumeUpdateUsers(context.TODO(), s, projectName, newPool.Name(), &newVol, pool.Name(), vol)
})
// Provide empty description and nil config to instruct CreateCustomVolumeFromCopy to copy it
// from source volume.
err = newPool.CreateCustomVolumeFromCopy(projectName, requestProjectName, newVol.Name, "", nil, pool.Name(), vol.Name, true, op)
if err != nil {
return err
}
err = pool.DeleteCustomVolume(requestProjectName, vol.Name, op)
if err != nil {
return err
}
reverter.Success()
return nil
}
op, err := operations.OperationCreate(s, requestProjectName, operations.OperationClassTask, operationtype.VolumeMove, nil, nil, run, nil, nil, r)
if err != nil {
return response.InternalError(err)
}
return operations.OperationResponse(op)
}
// swagger:operation GET /1.0/storage-pools/{poolName}/volumes/{type}/{volumeName} storage storage_pool_volume_type_get
//
// Get the storage volume
//
// Gets a specific storage volume.
//
// ---
// produces:
// - application/json
// parameters:
// - in: path
// name: poolName
// description: Storage pool name
// type: string
// required: true
// - in: path
// name: type
// description: Storage volume type
// type: string
// required: true
// - in: path
// name: volumeName
// description: Storage volume name
// type: string
// required: true
// - in: query
// name: project
// description: Project name
// type: string
// example: default
// - in: query
// name: target
// description: Cluster member name
// type: string
// example: server01
// responses:
// "200":
// description: Storage volume
// schema:
// type: object
// description: Sync response
// properties:
// type:
// type: string
// description: Response type
// example: sync
// status:
// type: string
// description: Status description
// example: Success
// status_code:
// type: integer
// description: Status code
// example: 200
// metadata:
// $ref: "#/definitions/StorageVolume"
// "400":
// $ref: "#/responses/BadRequest"
// "403":
// $ref: "#/responses/Forbidden"
// "404":
// $ref: "#/responses/NotFound"
// "409":
// $ref: "#/responses/Conflict"
// "500":
// $ref: "#/responses/InternalServerError"
// swagger:operation GET /1.0/storage-pools/{poolName}/volumes/{type}/{volumeName}?recursion=1 storage storage_pool_volume_type_get_recursion1
//
// Get the full storage volume details
//
// Gets a specific storage volume with all details (backups, snapshots and state0..
//
// ---
// produces:
// - application/json
// parameters:
// - in: path
// name: poolName
// description: Storage pool name
// type: string
// required: true
// - in: path
// name: type
// description: Storage volume type
// type: string
// required: true
// - in: path
// name: volumeName
// description: Storage volume name
// type: string
// required: true
// - in: query
// name: project
// description: Project name
// type: string
// example: default
// - in: query
// name: target
// description: Cluster member name
// type: string
// example: server01
// responses:
// "200":
// description: Storage volume
// schema:
// type: object
// description: Sync response
// properties:
// type:
// type: string
// description: Response type
// example: sync
// status:
// type: string
// description: Status description
// example: Success
// status_code:
// type: integer
// description: Status code
// example: 200
// metadata:
// $ref: "#/definitions/StorageVolumeFull"
// "400":
// $ref: "#/responses/BadRequest"
// "403":
// $ref: "#/responses/Forbidden"
// "404":
// $ref: "#/responses/NotFound"
// "409":
// $ref: "#/responses/Conflict"
// "500":
// $ref: "#/responses/InternalServerError"
func storagePoolVolumeGet(d *Daemon, r *http.Request) response.Response {
s := d.State()
volumeTypeName, err := pathVar(r, "type")
if err != nil {
return response.SmartError(err)
}
// Get the name of the storage volume.
volumeName, err := pathVar(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 := pathVar(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 valid.
if !slices.Contains(supportedVolumeTypes, volumeType) {
return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName))
}
requestProjectName := request.ProjectParam(r)
volumeProjectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, volumeType)
if err != nil {
return response.SmartError(err)
}
resp := forwardedResponseIfTargetIsRemote(s, r)
if resp != nil {
return resp
}
resp = forwardedResponseIfVolumeIsRemote(s, r, poolName, volumeProjectName, volumeName, volumeType)
if resp != nil {
return resp
}
var dbVolume *db.StorageVolume
var poolID int64
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
var err 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, volumeProjectName, volumeType, volumeName, true)
return err
})
if err != nil {
return response.SmartError(err)
}
volumeUsedBy, err := storagePoolVolumeUsedByGet(s, requestProjectName, poolName, dbVolume)
if err != nil {
return response.SmartError(err)
}
dbVolume.UsedBy = project.FilterUsedBy(s.Authorizer, r, volumeUsedBy)
etag := []any{volumeName, dbVolume.Type, dbVolume.Config}
// Prepare the response.
if localUtil.IsRecursionRequest(r) {
volFull, err := getVolumeFull(r.Context(), s, poolName, dbVolume.StorageVolume)
if err != nil {
return response.SmartError(err)
}
return response.SyncResponseETag(true, volFull, etag)
}
return response.SyncResponseETag(true, dbVolume.StorageVolume, etag)
}
func getVolumeFull(ctx context.Context, s *state.State, poolName string, vol api.StorageVolume) (*api.StorageVolumeFull, error) {
// Convert the volume type name to our internal integer representation.
volType, err := storagePools.VolumeTypeNameToDBType(vol.Type)
if err != nil {
return nil, err
}
// Set the base object.
resp := api.StorageVolumeFull{
StorageVolume: vol,
Backups: []api.StorageVolumeBackup{},
}
// Get the pool.
pool, err := storagePools.LoadByName(s, poolName)
if err != nil {
return nil, err
}
// Add all backups.
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
volumeBackups, err := tx.GetStoragePoolVolumeBackups(ctx, vol.Project, vol.Name, pool.ID())
if err != nil {
return err
}
for _, entry := range volumeBackups {
resp.Backups = append(resp.Backups, *backup.NewVolumeBackup(s, vol.Project, poolName, vol.Name, entry.ID, entry.Name, entry.CreationDate, entry.ExpiryDate, entry.VolumeOnly, entry.OptimizedStorage).Render())
}
return nil
})
if err != nil {
return nil, err
}
// Add all snapshots.
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
volumeSnapshots, err := tx.GetLocalStoragePoolVolumeSnapshotsWithType(ctx, vol.Project, vol.Name, volType, pool.ID())
if err != nil {
return err
}
for _, entry := range volumeSnapshots {
_, snapName, _ := api.GetParentAndSnapshotName(entry.Name)
snap := api.StorageVolumeSnapshot{}
snap.Config = entry.Config
snap.Description = entry.Description
snap.Name = snapName
snap.CreatedAt = entry.CreationDate
if entry.ExpiryDate.Unix() > 0 {
snap.ExpiresAt = &entry.ExpiryDate
}
resp.Snapshots = append(resp.Snapshots, snap)
}
return nil
})
if err != nil {
return nil, err
}
// Add the state.
var usage *storagePools.VolumeUsage
switch volType {
case db.StoragePoolVolumeTypeCustom:
usage, err = pool.GetCustomVolumeUsage(vol.Project, vol.Name)
if err != nil && !errors.Is(err, storageDrivers.ErrNotSupported) {
return nil, err
}
case db.StoragePoolVolumeTypeContainer, db.StoragePoolVolumeTypeVM:
inst, err := instance.LoadByProjectAndName(s, vol.Project, vol.Name)
if err != nil {
return nil, err
}
usage, err = pool.GetInstanceUsage(inst)
if err != nil && !errors.Is(err, storageDrivers.ErrNotSupported) {
return nil, err
}
default:
}
volState := api.StorageVolumeState{}
if usage != nil {
volState.Usage = &api.StorageVolumeStateUsage{}
// Only fill 'used' field if receiving a valid value.
if usage.Used >= 0 {
volState.Usage.Used = uint64(usage.Used)
}
// Only fill 'total' field if receiving a valid value.
if usage.Total >= 0 {
volState.Usage.Total = usage.Total
}
}
resp.State = &volState
return &resp, nil
}
// swagger:operation PUT /1.0/storage-pools/{poolName}/volumes/{type}/{volumeName} storage storage_pool_volume_type_put
//
// Update the storage volume
//
// Updates the entire storage volume configuration.
//
// ---
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - in: path
// name: poolName
// description: Storage pool name
// type: string
// required: true
// - in: path
// name: type
// description: Storage volume type
// type: string
// required: true
// - in: path
// name: volumeName
// description: Storage volume name
// type: string
// required: true
// - in: query
// name: project
// description: Project name
// type: string
// example: default
// - in: query
// name: target
// description: Cluster member name
// type: string
// example: server01
// - in: body
// name: storage volume
// description: Storage volume configuration
// required: true
// schema:
// $ref: "#/definitions/StorageVolumePut"
// responses:
// "200":
// $ref: "#/responses/EmptySyncResponse"
// "400":
// $ref: "#/responses/BadRequest"
// "403":
// $ref: "#/responses/Forbidden"
// "404":
// $ref: "#/responses/NotFound"
// "409":
// $ref: "#/responses/Conflict"
// "412":
// $ref: "#/responses/PreconditionFailed"
// "500":
// $ref: "#/responses/InternalServerError"
func storagePoolVolumePut(d *Daemon, r *http.Request) response.Response {
s := d.State()
projectName := request.ProjectParam(r)
volumeTypeName, err := pathVar(r, "type")
if err != nil {
return response.SmartError(err)
}
// Get the name of the storage volume.
volumeName, err := pathVar(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 := pathVar(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)
}
projectName, err = project.StorageVolumeProject(s.DB.Cluster, projectName, volumeType)
if err != nil {
return response.SmartError(err)
}
// Check that the storage volume type is valid.
if !slices.Contains(supportedVolumeTypes, volumeType) {
return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName))
}
pool, err := storagePools.LoadByName(s, poolName)
if err != nil {
return response.SmartError(err)
}
resp := forwardedResponseIfTargetIsRemote(s, r)
if resp != nil {
return resp
}
resp = forwardedResponseIfVolumeIsRemote(s, r, pool.Name(), projectName, volumeName, volumeType)
if resp != nil {
return resp
}
// 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)
return err
})
if err != nil {
return response.SmartError(err)
}
// Validate the ETag
etag := []any{volumeName, dbVolume.Type, dbVolume.Config}
err = localUtil.EtagCheck(r, etag)
if err != nil {
return response.PreconditionFailed(err)
}
req := api.StorageVolumePut{}
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
return response.BadRequest(err)
}
// Use an empty operation for this sync response to pass the requestor
op := &operations.Operation{}
op.SetRequestor(r)
switch volumeType {
case db.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)
if err != nil {
return response.SmartError(err)
}
}
// Handle custom volume update requests.
// Only apply changes during a snapshot restore if a non-nil config is supplied to avoid clearing
// the volume's config if only restoring snapshot.
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(tx, projectName, volumeName, req, dbVolume.Config)
})
if err != nil {
return response.SmartError(err)
}
err = pool.UpdateCustomVolume(projectName, dbVolume.Name, req.Description, req.Config, op)
if err != nil {
return response.SmartError(err)
}
}
case db.StoragePoolVolumeTypeContainer, db.StoragePoolVolumeTypeVM:
inst, err := instance.LoadByProjectAndName(s, projectName, dbVolume.Name)
if err != nil {
return response.SmartError(err)
}
// Handle instance volume update requests.
err = pool.UpdateInstance(inst, req.Description, req.Config, op)
if err != nil {
return response.SmartError(err)
}
case db.StoragePoolVolumeTypeImage:
// Handle image update requests.
err = pool.UpdateImage(dbVolume.Name, req.Description, req.Config, op)
if err != nil {
return response.SmartError(err)
}
default:
return response.SmartError(errors.New("Invalid volume type"))
}
return response.EmptySyncResponse
}
// swagger:operation PATCH /1.0/storage-pools/{poolName}/volumes/{type}/{volumeName} storage storage_pool_volume_type_patch
//
// Partially update the storage volume
//
// Updates a subset of the storage volume configuration.
//
// ---
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - in: path
// name: poolName
// description: Storage pool name
// type: string
// required: true
// - in: path
// name: type
// description: Storage volume type
// type: string
// required: true
// - in: path
// name: volumeName
// description: Storage volume name
// type: string
// required: true
// - in: query
// name: project
// description: Project name
// type: string
// example: default
// - in: query
// name: target
// description: Cluster member name
// type: string
// example: server01
// - in: body
// name: storage volume
// description: Storage volume configuration
// required: true
// schema:
// $ref: "#/definitions/StorageVolumePut"
// responses:
// "200":
// $ref: "#/responses/EmptySyncResponse"
// "400":
// $ref: "#/responses/BadRequest"
// "403":
// $ref: "#/responses/Forbidden"
// "404":
// $ref: "#/responses/NotFound"
// "409":
// $ref: "#/responses/Conflict"
// "412":
// $ref: "#/responses/PreconditionFailed"
// "500":
// $ref: "#/responses/InternalServerError"
func storagePoolVolumePatch(d *Daemon, r *http.Request) response.Response {
s := d.State()
// Get the name of the storage volume.
volumeName, err := pathVar(r, "volumeName")
if err != nil {
return response.SmartError(err)
}
volumeTypeName, err := pathVar(r, "type")
if err != nil {
return response.SmartError(err)
}
if internalInstance.IsSnapshot(volumeName) {
return response.BadRequest(errors.New("Invalid volume name"))
}
// Get the name of the storage pool the volume is supposed to be attached to.
poolName, err := pathVar(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 != db.StoragePoolVolumeTypeCustom {
return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName))
}
projectName, err := project.StorageVolumeProject(s.DB.Cluster, request.ProjectParam(r), volumeType)
if err != nil {
return response.SmartError(err)
}
pool, err := storagePools.LoadByName(s, poolName)
if err != nil {
return response.SmartError(err)
}
resp := forwardedResponseIfTargetIsRemote(s, r)
if resp != nil {
return resp
}
resp = forwardedResponseIfVolumeIsRemote(s, r, pool.Name(), projectName, volumeName, volumeType)
if resp != nil {
return resp
}
// 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)
return err
})
if err != nil {
return response.SmartError(err)
}
// Validate the ETag.
etag := []any{volumeName, dbVolume.Type, dbVolume.Config}
err = localUtil.EtagCheck(r, etag)
if err != nil {
return response.PreconditionFailed(err)
}
req := api.StorageVolumePut{}
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
return response.BadRequest(err)
}
if req.Config == nil {
req.Config = map[string]string{}
}
// Merge current config with requested changes.
for k, v := range dbVolume.Config {
_, ok := req.Config[k]
if !ok {
req.Config[k] = v
}
}
// Use an empty operation for this sync response to pass the requestor
op := &operations.Operation{}
op.SetRequestor(r)
err = pool.UpdateCustomVolume(projectName, dbVolume.Name, req.Description, req.Config, op)
if err != nil {
return response.SmartError(err)
}
return response.EmptySyncResponse
}
// swagger:operation DELETE /1.0/storage-pools/{poolName}/volumes/{type}/{volumeName} storage storage_pool_volume_type_delete
//
// Delete the storage volume
//
// Removes the storage volume.
//
// ---
// produces:
// - application/json
// parameters:
// - in: path
// name: poolName
// description: Storage pool name
// type: string
// required: true
// - in: path
// name: type
// description: Storage volume type
// type: string
// required: true
// - in: path
// name: volumeName
// description: Storage volume name
// type: string
// required: true
// - in: query
// name: project
// description: Project name
// type: string
// example: default
// - in: query
// name: target
// description: Cluster member name
// type: string
// example: server01
// responses:
// "200":
// $ref: "#/responses/EmptySyncResponse"
// "400":
// $ref: "#/responses/BadRequest"
// "403":
// $ref: "#/responses/Forbidden"
// "404":
// $ref: "#/responses/NotFound"
// "409":
// $ref: "#/responses/Conflict"
// "500":
// $ref: "#/responses/InternalServerError"
func storagePoolVolumeDelete(d *Daemon, r *http.Request) response.Response {
s := d.State()
// Get the name of the storage volume.
volumeName, err := pathVar(r, "volumeName")
if err != nil {
return response.SmartError(err)
}
volumeTypeName, err := pathVar(r, "type")
if err != nil {
return response.SmartError(err)
}
if internalInstance.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 := pathVar(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)
volumeProjectName, err := project.StorageVolumeProject(s.DB.Cluster, requestProjectName, volumeType)
if err != nil {
return response.SmartError(err)
}
// Check that the storage volume type is valid.
if !slices.Contains(supportedVolumeTypes, volumeType) {
return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", volumeTypeName))
}
resp := forwardedResponseIfTargetIsRemote(s, r)
if resp != nil {
return resp
}
resp = forwardedResponseIfVolumeIsRemote(s, r, poolName, volumeProjectName, volumeName, volumeType)
if resp != nil {
return resp
}
if volumeType != db.StoragePoolVolumeTypeCustom && volumeType != db.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)
if err != nil {
return response.SmartError(err)
}
// 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(), volumeProjectName, volumeType, volumeName, true)
return err
})
if err != nil {
return response.SmartError(err)
}
volumeUsedBy, err := storagePoolVolumeUsedByGet(s, requestProjectName, poolName, dbVolume)
if err != nil {
return response.SmartError(err)
}
// isImageURL checks whether the provided usedByURL represents an image resource for the fingerprint.
isImageURL := func(usedByURL string, fingerprint string) bool {
usedBy, _ := url.Parse(usedByURL)
if usedBy == nil {
return false
}
img := api.NewURL().Path(version.APIVersion, "images", fingerprint)
return usedBy.Path == img.URL.Path
}
if len(volumeUsedBy) > 0 {
if len(volumeUsedBy) != 1 || volumeType != db.StoragePoolVolumeTypeImage || !isImageURL(volumeUsedBy[0], dbVolume.Name) {
return response.BadRequest(errors.New("The storage volume is still in use"))
}
}
// Use an empty operation for this sync response to pass the requestor
op := &operations.Operation{}
op.SetRequestor(r)
switch volumeType {
case db.StoragePoolVolumeTypeCustom:
err = pool.DeleteCustomVolume(volumeProjectName, volumeName, op)
case db.StoragePoolVolumeTypeImage:
err = pool.DeleteImage(volumeName, op)
default:
return response.BadRequest(fmt.Errorf(`Storage volumes of type %q cannot be deleted with the storage API`, volumeTypeName))
}
if err != nil {
return response.SmartError(err)
}
return response.EmptySyncResponse
}
func createStoragePoolVolumeFromISO(s *state.State, r *http.Request, requestProjectName string, projectName string, data io.Reader, pool string, volName string) response.Response {
reverter := revert.New()
defer reverter.Fail()
if volName == "" {
return response.BadRequest(errors.New("Missing volume name"))
}
// Validate the volume name to avoid path traversal when used as a path segment.
err := validate.IsAPIName(volName, false)
if err != nil {
return response.BadRequest(fmt.Errorf("Invalid volume name: %w", err))
}
// Create isos directory if needed.
if !util.PathExists(internalUtil.VarPath("isos")) {
err := os.MkdirAll(internalUtil.VarPath("isos"), 0o644)
if err != nil {
return response.InternalError(err)
}
}
// Create temporary file to store uploaded ISO data.
isoFile, err := os.CreateTemp(internalUtil.VarPath("isos"), fmt.Sprintf("%s_", "incus_iso"))
if err != nil {
return response.InternalError(err)
}
defer logger.WarnOnError(func() error { return os.Remove(isoFile.Name()) }, "Failed to remove ISO file")
reverter.Add(func() { _ = isoFile.Close() })
// Get disk budget for the project if any.
var budget int64
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
budget, err = project.GetSpaceBudget(tx, projectName)
if err != nil {
return err
}
return nil
})
if err != nil {
return response.InternalError(err)
}
// Stream uploaded ISO data into temporary file.
size, err := util.SafeCopy(internalIO.NewQuotaWriter(isoFile, budget), data)
if err != nil {
return response.InternalError(err)
}
// Copy reverter so far so we can use it inside run after this function has finished.
runReverter := reverter.Clone()
run := func(op *operations.Operation) error {
defer logger.WarnOnError(isoFile.Close, "Failed to close ISO file")
defer runReverter.Fail()
pool, err := storagePools.LoadByName(s, pool)
if err != nil {
return err
}
// Dump ISO to storage.
err = pool.CreateCustomVolumeFromISO(projectName, volName, isoFile, size, op)
if err != nil {
return fmt.Errorf("Failed creating custom volume from ISO: %w", err)
}
runReverter.Success()
return nil
}
resources := map[string][]api.URL{}
resources["storage_volumes"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", pool, "volumes", "custom", volName)}
op, err := operations.OperationCreate(s, requestProjectName, operations.OperationClassTask, operationtype.VolumeCreate, resources, nil, run, nil, nil, r)
if err != nil {
return response.InternalError(err)
}
reverter.Success()
return operations.OperationResponse(op)
}
func createStoragePoolVolumeFromBackup(s *state.State, r *http.Request, requestProjectName string, projectName string, data io.Reader, pool string, volName string) response.Response {
reverter := revert.New()
defer reverter.Fail()
// Create temporary file to store uploaded backup data.
backupFile, err := os.CreateTemp(internalUtil.VarPath("backups"), fmt.Sprintf("%s_", backup.WorkingDirPrefix))
if err != nil {
return response.InternalError(err)
}
defer logger.WarnOnError(func() error { return os.Remove(backupFile.Name()) }, "Failed to remove backup file")
reverter.Add(func() { _ = backupFile.Close() })
// Get disk budget for the project if any.
var budget int64
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
budget, err = project.GetSpaceBudget(tx, projectName)
if err != nil {
return err
}
return nil
})
if err != nil {
return response.InternalError(err)
}
// Stream uploaded backup data into temporary file.
_, err = util.SafeCopy(internalIO.NewQuotaWriter(backupFile, budget), data)
if err != nil {
return response.InternalError(err)
}
// Detect squashfs compression and convert to tarball.
_, err = backupFile.Seek(0, io.SeekStart)
if err != nil {
return response.InternalError(err)
}
_, algo, decomArgs, err := archive.DetectCompressionFile(backupFile)
if err != nil {
return response.InternalError(err)
}
if algo == ".squashfs" {
// Pass the temporary file as program argument to the decompression command.
decomArgs := append(decomArgs, backupFile.Name())
// Create temporary file to store the decompressed tarball in.
tarFile, err := os.CreateTemp(internalUtil.VarPath("backups"), fmt.Sprintf("%s_decompress_", backup.WorkingDirPrefix))
if err != nil {
return response.InternalError(err)
}
defer logger.WarnOnError(func() error { return os.Remove(tarFile.Name()) }, "Failed to remove tarball file")
// Decompress to tarFile temporary file.
err = archive.ExtractWithFds(decomArgs[0], decomArgs[1:], nil, nil, tarFile)
if err != nil {
return response.InternalError(err)
}
// We don't need the original squashfs file anymore.
_ = backupFile.Close()
_ = os.Remove(backupFile.Name())
// Replace the backup file handle with the handle to the tar file.
backupFile = tarFile
}
// Parse the backup information.
_, err = backupFile.Seek(0, io.SeekStart)
if err != nil {
return response.InternalError(err)
}
logger.Debug("Reading backup file info")
bInfo, err := backup.GetInfo(backupFile, s.OS, backupFile.Name())
if err != nil {
return response.BadRequest(err)
}
bInfo.Project = projectName
// Override pool.
if pool != "" {
bInfo.Pool = pool
}
// Override volume name.
if volName != "" {
bInfo.Name = volName
}
// Validate the volume and snapshot names to avoid path traversal when used as path segments.
err = validate.IsAPIName(bInfo.Name, false)
if err != nil {
return response.BadRequest(fmt.Errorf("Invalid volume name: %w", err))
}
for _, snapName := range bInfo.Snapshots {
err = validate.IsAPIName(snapName, false)
if err != nil {
return response.BadRequest(fmt.Errorf("Invalid snapshot name: %w", err))
}
}
logger.Debug("Backup file info loaded", logger.Ctx{
"type": bInfo.Type,
"name": bInfo.Name,
"project": bInfo.Project,
"backend": bInfo.Backend,
"pool": bInfo.Pool,
"optimized": *bInfo.OptimizedStorage,
"snapshots": bInfo.Snapshots,
})
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
// Check storage pool exists.
_, _, _, err = tx.GetStoragePoolInAnyState(ctx, bInfo.Pool)
return err
})
if response.IsNotFoundError(err) {
// The storage pool doesn't exist. If backup is in binary format (so we cannot alter
// the backup.yaml) or the pool has been specified directly from the user restoring
// the backup then we cannot proceed so return an error.
if *bInfo.OptimizedStorage || pool != "" {
return response.InternalError(fmt.Errorf("Storage pool not found: %w", err))
}
var profile *api.Profile
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
// Otherwise try and restore to the project's default profile pool.
_, profile, err = tx.GetProfile(ctx, bInfo.Project, "default")
return err
})
if err != nil {
return response.InternalError(fmt.Errorf("Failed to get default profile: %w", err))
}
_, v, err := internalInstance.GetRootDiskDevice(profile.Devices)
if err != nil {
return response.InternalError(fmt.Errorf("Failed to get root disk device: %w", err))
}
// Use the default-profile's root pool.
bInfo.Pool = v["pool"]
} else if err != nil {
return response.InternalError(err)
}
// Copy reverter so far so we can use it inside run after this function has finished.
runReverter := reverter.Clone()
run := func(op *operations.Operation) error {
defer logger.WarnOnError(backupFile.Close, "Failed to close backup file")
defer runReverter.Fail()
pool, err := storagePools.LoadByName(s, bInfo.Pool)
if err != nil {
return err
}
// Check if the backup is optimized that the source pool driver matches the target pool driver.
if *bInfo.OptimizedStorage && pool.Driver().Info().Name != bInfo.Backend {
return fmt.Errorf("Optimized backup storage driver %q differs from the target storage pool driver %q", bInfo.Backend, pool.Driver().Info().Name)
}
// Dump tarball to storage.
err = pool.CreateCustomVolumeFromBackup(*bInfo, backupFile, backup.DefaultBackupPrefix, nil)
if err != nil {
return fmt.Errorf("Create custom volume from backup: %w", err)
}
runReverter.Success()
return nil
}
resources := map[string][]api.URL{}
resources["storage_volumes"] = []api.URL{*api.NewURL().Path(version.APIVersion, "storage-pools", bInfo.Pool, "volumes", string(bInfo.Type), bInfo.Name)}
op, err := operations.OperationCreate(s, requestProjectName, operations.OperationClassTask, operationtype.CustomVolumeBackupRestore, resources, nil, run, nil, nil, r)
if err != nil {
return response.InternalError(err)
}
reverter.Success()
return operations.OperationResponse(op)
}