github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/lxd/storage.go (about)

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package lxd
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/juju/collections/set"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/schema"
    13  	"github.com/lxc/lxd/shared"
    14  	"github.com/lxc/lxd/shared/api"
    15  	"gopkg.in/juju/names.v2"
    16  
    17  	"github.com/juju/juju/container/lxd"
    18  	"github.com/juju/juju/core/instance"
    19  	"github.com/juju/juju/environs"
    20  	"github.com/juju/juju/environs/context"
    21  	"github.com/juju/juju/environs/tags"
    22  	"github.com/juju/juju/provider/common"
    23  	"github.com/juju/juju/storage"
    24  )
    25  
    26  const (
    27  	lxdStorageProviderType = "lxd"
    28  
    29  	// attrLXDStorageDriver is the attribute name for the
    30  	// storage pool's LXD storage driver. This and "lxd-pool"
    31  	// are the only predefined storage attributes; all others
    32  	// are passed on to LXD directly.
    33  	attrLXDStorageDriver = "driver"
    34  
    35  	// attrLXDStoragePool is the attribute name for the
    36  	// storage pool's corresponding LXD storage pool name.
    37  	// If this is not provided, the LXD storage pool name
    38  	// will be set to "juju".
    39  	attrLXDStoragePool = "lxd-pool"
    40  
    41  	storagePoolVolumeType = "custom"
    42  )
    43  
    44  func (env *environ) storageSupported() bool {
    45  	return env.server.StorageSupported()
    46  }
    47  
    48  // StorageProviderTypes implements storage.ProviderRegistry.
    49  func (env *environ) StorageProviderTypes() ([]storage.ProviderType, error) {
    50  	var types []storage.ProviderType
    51  	if env.storageSupported() {
    52  		types = append(types, lxdStorageProviderType)
    53  	}
    54  	return types, nil
    55  }
    56  
    57  // StorageProvider implements storage.ProviderRegistry.
    58  func (env *environ) StorageProvider(t storage.ProviderType) (storage.Provider, error) {
    59  	if env.storageSupported() && t == lxdStorageProviderType {
    60  		return &lxdStorageProvider{env}, nil
    61  	}
    62  	return nil, errors.NotFoundf("storage provider %q", t)
    63  }
    64  
    65  // lxdStorageProvider is a storage provider for LXD volumes, exposed to Juju as
    66  // filesystems.
    67  type lxdStorageProvider struct {
    68  	env *environ
    69  }
    70  
    71  var _ storage.Provider = (*lxdStorageProvider)(nil)
    72  
    73  var lxdStorageConfigFields = schema.Fields{
    74  	attrLXDStorageDriver: schema.OneOf(
    75  		schema.Const("zfs"),
    76  		schema.Const("dir"),
    77  		schema.Const("btrfs"),
    78  		schema.Const("lvm"),
    79  	),
    80  	attrLXDStoragePool: schema.String(),
    81  }
    82  
    83  var lxdStorageConfigChecker = schema.FieldMap(
    84  	lxdStorageConfigFields,
    85  	schema.Defaults{
    86  		attrLXDStorageDriver: "dir",
    87  		attrLXDStoragePool:   schema.Omit,
    88  	},
    89  )
    90  
    91  type lxdStorageConfig struct {
    92  	lxdPool string
    93  	driver  string
    94  	attrs   map[string]string
    95  }
    96  
    97  func newLXDStorageConfig(attrs map[string]interface{}) (*lxdStorageConfig, error) {
    98  	coerced, err := lxdStorageConfigChecker.Coerce(attrs, nil)
    99  	if err != nil {
   100  		return nil, errors.Annotate(err, "validating Azure storage config")
   101  	}
   102  	attrs = coerced.(map[string]interface{})
   103  
   104  	driver := attrs[attrLXDStorageDriver].(string)
   105  	lxdPool, _ := attrs[attrLXDStoragePool].(string)
   106  	delete(attrs, attrLXDStorageDriver)
   107  	delete(attrs, attrLXDStoragePool)
   108  
   109  	var stringAttrs map[string]string
   110  	if len(attrs) > 0 {
   111  		stringAttrs = make(map[string]string)
   112  		for k, v := range attrs {
   113  			if vString, ok := v.(string); ok {
   114  				stringAttrs[k] = vString
   115  			} else {
   116  				stringAttrs[k] = fmt.Sprint(v)
   117  			}
   118  		}
   119  	}
   120  
   121  	if lxdPool == "" {
   122  		lxdPool = "juju"
   123  	}
   124  
   125  	lxdStorageConfig := &lxdStorageConfig{
   126  		lxdPool: lxdPool,
   127  		driver:  driver,
   128  		attrs:   stringAttrs,
   129  	}
   130  	return lxdStorageConfig, nil
   131  }
   132  
   133  // ValidateConfig is part of the Provider interface.
   134  func (e *lxdStorageProvider) ValidateConfig(cfg *storage.Config) error {
   135  	lxdStorageConfig, err := newLXDStorageConfig(cfg.Attrs())
   136  	if err != nil {
   137  		return errors.Trace(err)
   138  	}
   139  	return ensureLXDStoragePool(e.env, lxdStorageConfig)
   140  }
   141  
   142  // Supports is part of the Provider interface.
   143  func (e *lxdStorageProvider) Supports(k storage.StorageKind) bool {
   144  	return k == storage.StorageKindFilesystem
   145  }
   146  
   147  // Scope is part of the Provider interface.
   148  func (e *lxdStorageProvider) Scope() storage.Scope {
   149  	return storage.ScopeEnviron
   150  }
   151  
   152  // Dynamic is part of the Provider interface.
   153  func (e *lxdStorageProvider) Dynamic() bool {
   154  	return true
   155  }
   156  
   157  // Releasable is defined on the Provider interface.
   158  func (*lxdStorageProvider) Releasable() bool {
   159  	return true
   160  }
   161  
   162  // DefaultPools is part of the Provider interface.
   163  func (e *lxdStorageProvider) DefaultPools() []*storage.Config {
   164  	zfsPool, _ := storage.NewConfig("lxd-zfs", lxdStorageProviderType, map[string]interface{}{
   165  		attrLXDStorageDriver: "zfs",
   166  		attrLXDStoragePool:   "juju-zfs",
   167  		"zfs.pool_name":      "juju-lxd",
   168  	})
   169  	btrfsPool, _ := storage.NewConfig("lxd-btrfs", lxdStorageProviderType, map[string]interface{}{
   170  		attrLXDStorageDriver: "btrfs",
   171  		attrLXDStoragePool:   "juju-btrfs",
   172  	})
   173  
   174  	var pools []*storage.Config
   175  	if e.ValidateConfig(zfsPool) == nil {
   176  		pools = append(pools, zfsPool)
   177  	}
   178  	if e.ValidateConfig(btrfsPool) == nil {
   179  		pools = append(pools, btrfsPool)
   180  	}
   181  	return pools
   182  }
   183  
   184  // VolumeSource is part of the Provider interface.
   185  func (e *lxdStorageProvider) VolumeSource(cfg *storage.Config) (storage.VolumeSource, error) {
   186  	return nil, errors.NotSupportedf("volumes")
   187  }
   188  
   189  // FilesystemSource is part of the Provider interface.
   190  func (e *lxdStorageProvider) FilesystemSource(cfg *storage.Config) (storage.FilesystemSource, error) {
   191  	return &lxdFilesystemSource{e.env}, nil
   192  }
   193  
   194  func ensureLXDStoragePool(env *environ, cfg *lxdStorageConfig) error {
   195  	createErr := env.server.CreatePool(cfg.lxdPool, cfg.driver, cfg.attrs)
   196  	if createErr == nil {
   197  		return nil
   198  	}
   199  	// There's no specific error to check for, so we just assume
   200  	// that the error is due to the pool already existing, and
   201  	// verify that. If it doesn't exist, return the original
   202  	// CreateStoragePool error.
   203  
   204  	pool, _, err := env.server.GetStoragePool(cfg.lxdPool)
   205  	if lxd.IsLXDNotFound(err) {
   206  		return errors.Annotatef(createErr, "creating LXD storage pool %q", cfg.lxdPool)
   207  	} else if err != nil {
   208  		return errors.Annotatef(createErr, "getting storage pool %q", cfg.lxdPool)
   209  	}
   210  	// The storage pool already exists: check that the existing pool's
   211  	// driver and config match what we want.
   212  	if pool.Driver != cfg.driver {
   213  		return errors.Errorf(
   214  			`LXD storage pool %q exists, with conflicting driver %q. Specify an alternative pool name via the "lxd-pool" attribute.`,
   215  			pool.Name, pool.Driver,
   216  		)
   217  	}
   218  	for k, v := range cfg.attrs {
   219  		if haveV, ok := pool.Config[k]; !ok || haveV != v {
   220  			return errors.Errorf(
   221  				`LXD storage pool %q exists, with conflicting config attribute %q=%q. Specify an alternative pool name via the "lxd-pool" attribute.`,
   222  				pool.Name, k, haveV,
   223  			)
   224  		}
   225  	}
   226  	return nil
   227  }
   228  
   229  type lxdFilesystemSource struct {
   230  	env *environ
   231  }
   232  
   233  // CreateFilesystems is specified on the storage.FilesystemSource interface.
   234  func (s *lxdFilesystemSource) CreateFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemParams) (_ []storage.CreateFilesystemsResult, err error) {
   235  	results := make([]storage.CreateFilesystemsResult, len(args))
   236  	for i, arg := range args {
   237  		if err := s.ValidateFilesystemParams(arg); err != nil {
   238  			results[i].Error = err
   239  			continue
   240  		}
   241  		filesystem, err := s.createFilesystem(arg)
   242  		if err != nil {
   243  			results[i].Error = err
   244  			common.HandleCredentialError(IsAuthorisationFailure, err, ctx)
   245  			continue
   246  		}
   247  		results[i].Filesystem = filesystem
   248  	}
   249  	return results, nil
   250  }
   251  
   252  func (s *lxdFilesystemSource) createFilesystem(
   253  	arg storage.FilesystemParams,
   254  ) (*storage.Filesystem, error) {
   255  
   256  	cfg, err := newLXDStorageConfig(arg.Attributes)
   257  	if err != nil {
   258  		return nil, errors.Trace(err)
   259  	}
   260  	if err := ensureLXDStoragePool(s.env, cfg); err != nil {
   261  		return nil, errors.Trace(err)
   262  	}
   263  
   264  	// The filesystem ID needs to be something unique, since there
   265  	// could be multiple models creating volumes within the same
   266  	// LXD storage pool.
   267  	volumeName := s.env.namespace.Value(arg.Tag.String())
   268  	filesystemId := makeFilesystemId(cfg, volumeName)
   269  
   270  	config := map[string]string{}
   271  	for k, v := range arg.ResourceTags {
   272  		config["user."+k] = v
   273  	}
   274  	switch cfg.driver {
   275  	case "dir":
   276  		// NOTE(axw) for the "dir" driver, the size attribute is rejected
   277  		// by LXD. Ideally LXD would be able to tell us the total size of
   278  		// the filesystem on which the directory was created, though.
   279  	default:
   280  		config["size"] = fmt.Sprintf("%dMiB", arg.Size)
   281  	}
   282  
   283  	if err := s.env.server.CreateVolume(cfg.lxdPool, volumeName, config); err != nil {
   284  		return nil, errors.Annotate(err, "creating volume")
   285  	}
   286  
   287  	filesystem := storage.Filesystem{
   288  		Tag: arg.Tag,
   289  		FilesystemInfo: storage.FilesystemInfo{
   290  			FilesystemId: filesystemId,
   291  			Size:         arg.Size,
   292  		},
   293  	}
   294  	return &filesystem, nil
   295  }
   296  
   297  func makeFilesystemId(cfg *lxdStorageConfig, volumeName string) string {
   298  	// We need to include the LXD pool name in the filesystem ID,
   299  	// so that we can map it back to a volume.
   300  	return fmt.Sprintf("%s:%s", cfg.lxdPool, volumeName)
   301  }
   302  
   303  // parseFilesystemId parses the given filesystem ID, returning the underlying
   304  // LXD storage pool name and volume name.
   305  func parseFilesystemId(id string) (lxdPool, volumeName string, _ error) {
   306  	fields := strings.SplitN(id, ":", 2)
   307  	if len(fields) < 2 {
   308  		return "", "", errors.Errorf(
   309  			"invalid filesystem ID %q; expected ID in format <lxd-pool>:<volume-name>", id,
   310  		)
   311  	}
   312  	return fields[0], fields[1], nil
   313  }
   314  
   315  // TODO (manadart 2018-06-28) Add a test for DestroyController that properly
   316  // verifies this behaviour.
   317  func destroyControllerFilesystems(env *environ, controllerUUID string) error {
   318  	return errors.Trace(destroyFilesystems(env, func(v api.StorageVolume) bool {
   319  		return v.Config["user."+tags.JujuController] == controllerUUID
   320  	}))
   321  }
   322  
   323  func destroyModelFilesystems(env *environ) error {
   324  	return errors.Trace(destroyFilesystems(env, func(v api.StorageVolume) bool {
   325  		return v.Config["user."+tags.JujuModel] == env.Config().UUID()
   326  	}))
   327  }
   328  
   329  func destroyFilesystems(env *environ, match func(api.StorageVolume) bool) error {
   330  	pools, err := env.server.GetStoragePools()
   331  	if err != nil {
   332  		return errors.Annotate(err, "listing LXD storage pools")
   333  	}
   334  	for _, pool := range pools {
   335  		volumes, err := env.server.GetStoragePoolVolumes(pool.Name)
   336  		if err != nil {
   337  			return errors.Annotatef(err, "listing volumes in LXD storage pool %q", pool)
   338  		}
   339  		for _, volume := range volumes {
   340  			if !match(volume) {
   341  				continue
   342  			}
   343  			if err := env.server.DeleteStoragePoolVolume(pool.Name, storagePoolVolumeType, volume.Name); err != nil {
   344  				return errors.Annotatef(err, "deleting volume %q in LXD storage pool %q", volume.Name, pool)
   345  			}
   346  		}
   347  	}
   348  	return nil
   349  }
   350  
   351  // DestroyFilesystems is specified on the storage.FilesystemSource interface.
   352  func (s *lxdFilesystemSource) DestroyFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) {
   353  	results := make([]error, len(filesystemIds))
   354  	for i, filesystemId := range filesystemIds {
   355  		results[i] = s.destroyFilesystem(filesystemId)
   356  		common.HandleCredentialError(IsAuthorisationFailure, results[i], ctx)
   357  	}
   358  	return results, nil
   359  }
   360  
   361  func (s *lxdFilesystemSource) destroyFilesystem(filesystemId string) error {
   362  	poolName, volumeName, err := parseFilesystemId(filesystemId)
   363  	if err != nil {
   364  		return errors.Trace(err)
   365  	}
   366  	err = s.env.server.DeleteStoragePoolVolume(poolName, storagePoolVolumeType, volumeName)
   367  	if err != nil && !lxd.IsLXDNotFound(err) {
   368  		return errors.Trace(err)
   369  	}
   370  	return nil
   371  }
   372  
   373  // ReleaseFilesystems is specified on the storage.FilesystemSource interface.
   374  func (s *lxdFilesystemSource) ReleaseFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) {
   375  	results := make([]error, len(filesystemIds))
   376  	for i, filesystemId := range filesystemIds {
   377  		results[i] = s.releaseFilesystem(filesystemId)
   378  		common.HandleCredentialError(IsAuthorisationFailure, results[i], ctx)
   379  	}
   380  	return results, nil
   381  }
   382  
   383  func (s *lxdFilesystemSource) releaseFilesystem(filesystemId string) error {
   384  	poolName, volumeName, err := parseFilesystemId(filesystemId)
   385  	if err != nil {
   386  		return errors.Trace(err)
   387  	}
   388  	volume, eTag, err := s.env.server.GetStoragePoolVolume(poolName, storagePoolVolumeType, volumeName)
   389  	if err != nil {
   390  		return errors.Trace(err)
   391  	}
   392  	if volume.Config != nil {
   393  		delete(volume.Config, "user."+tags.JujuModel)
   394  		delete(volume.Config, "user."+tags.JujuController)
   395  		if err := s.env.server.UpdateStoragePoolVolume(
   396  			poolName, storagePoolVolumeType, volumeName, volume.Writable(), eTag); err != nil {
   397  			return errors.Annotatef(
   398  				err, "removing tags from volume %q in pool %q",
   399  				volumeName, poolName,
   400  			)
   401  		}
   402  	}
   403  	return nil
   404  }
   405  
   406  // ValidateFilesystemParams is specified on the storage.FilesystemSource interface.
   407  func (s *lxdFilesystemSource) ValidateFilesystemParams(params storage.FilesystemParams) error {
   408  	// TODO(axw) sanity check params
   409  	return nil
   410  }
   411  
   412  // AttachFilesystems is specified on the storage.FilesystemSource interface.
   413  func (s *lxdFilesystemSource) AttachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]storage.AttachFilesystemsResult, error) {
   414  	var instanceIds []instance.Id
   415  	instanceIdsSeen := make(set.Strings)
   416  	for _, arg := range args {
   417  		if instanceIdsSeen.Contains(string(arg.InstanceId)) {
   418  			continue
   419  		}
   420  		instanceIdsSeen.Add(string(arg.InstanceId))
   421  		instanceIds = append(instanceIds, arg.InstanceId)
   422  	}
   423  	instances, err := s.env.Instances(ctx, instanceIds)
   424  	switch err {
   425  	case nil, environs.ErrPartialInstances, environs.ErrNoInstances:
   426  	default:
   427  		common.HandleCredentialError(IsAuthorisationFailure, err, ctx)
   428  		return nil, errors.Trace(err)
   429  	}
   430  
   431  	results := make([]storage.AttachFilesystemsResult, len(args))
   432  	for i, arg := range args {
   433  		var inst *environInstance
   434  		for i, instanceId := range instanceIds {
   435  			if instanceId != arg.InstanceId {
   436  				continue
   437  			}
   438  			if instances[i] != nil {
   439  				inst = instances[i].(*environInstance)
   440  			}
   441  			break
   442  		}
   443  		attachment, err := s.attachFilesystem(arg, inst)
   444  		if err != nil {
   445  			results[i].Error = errors.Annotatef(
   446  				err, "attaching %s to %s",
   447  				names.ReadableString(arg.Filesystem),
   448  				names.ReadableString(arg.Machine),
   449  			)
   450  			common.HandleCredentialError(IsAuthorisationFailure, err, ctx)
   451  			continue
   452  		}
   453  		results[i].FilesystemAttachment = attachment
   454  	}
   455  	return results, nil
   456  }
   457  
   458  func (s *lxdFilesystemSource) attachFilesystem(
   459  	arg storage.FilesystemAttachmentParams,
   460  	inst *environInstance,
   461  ) (*storage.FilesystemAttachment, error) {
   462  	if inst == nil {
   463  		return nil, errors.NotFoundf("instance %q", arg.InstanceId)
   464  	}
   465  
   466  	poolName, volumeName, err := parseFilesystemId(arg.FilesystemId)
   467  	if err != nil {
   468  		return nil, errors.Trace(err)
   469  	}
   470  
   471  	deviceName := arg.Filesystem.String()
   472  	if err = inst.container.AddDisk(deviceName, arg.Path, volumeName, poolName, arg.ReadOnly); err != nil {
   473  		return nil, errors.Trace(err)
   474  	}
   475  
   476  	if err := s.env.server.WriteContainer(inst.container); err != nil {
   477  		return nil, errors.Trace(err)
   478  	}
   479  
   480  	filesystemAttachment := storage.FilesystemAttachment{
   481  		Filesystem: arg.Filesystem,
   482  		Machine:    arg.Machine,
   483  		FilesystemAttachmentInfo: storage.FilesystemAttachmentInfo{
   484  			Path:     arg.Path,
   485  			ReadOnly: arg.ReadOnly,
   486  		},
   487  	}
   488  	return &filesystemAttachment, nil
   489  }
   490  
   491  // DetachFilesystems is specified on the storage.FilesystemSource interface.
   492  func (s *lxdFilesystemSource) DetachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]error, error) {
   493  	var instanceIds []instance.Id
   494  	instanceIdsSeen := make(set.Strings)
   495  	for _, arg := range args {
   496  		if instanceIdsSeen.Contains(string(arg.InstanceId)) {
   497  			continue
   498  		}
   499  		instanceIdsSeen.Add(string(arg.InstanceId))
   500  		instanceIds = append(instanceIds, arg.InstanceId)
   501  	}
   502  	instances, err := s.env.Instances(ctx, instanceIds)
   503  	switch err {
   504  	case nil, environs.ErrPartialInstances, environs.ErrNoInstances:
   505  	default:
   506  		common.HandleCredentialError(IsAuthorisationFailure, err, ctx)
   507  		return nil, errors.Trace(err)
   508  	}
   509  
   510  	results := make([]error, len(args))
   511  	for i, arg := range args {
   512  		var inst *environInstance
   513  		for i, instanceId := range instanceIds {
   514  			if instanceId != arg.InstanceId {
   515  				continue
   516  			}
   517  			if instances[i] != nil {
   518  				inst = instances[i].(*environInstance)
   519  			}
   520  			break
   521  		}
   522  		if inst != nil {
   523  			err := s.detachFilesystem(arg, inst)
   524  			common.HandleCredentialError(IsAuthorisationFailure, err, ctx)
   525  			results[i] = errors.Annotatef(
   526  				err, "detaching %s",
   527  				names.ReadableString(arg.Filesystem),
   528  			)
   529  		}
   530  	}
   531  	return results, nil
   532  }
   533  
   534  func (s *lxdFilesystemSource) detachFilesystem(
   535  	arg storage.FilesystemAttachmentParams,
   536  	inst *environInstance,
   537  ) error {
   538  	deviceName := arg.Filesystem.String()
   539  	delete(inst.container.Devices, deviceName)
   540  	return errors.Trace(s.env.server.WriteContainer(inst.container))
   541  }
   542  
   543  // ImportFilesystem is part of the storage.FilesystemImporter interface.
   544  func (s *lxdFilesystemSource) ImportFilesystem(
   545  	callCtx context.ProviderCallContext,
   546  	filesystemId string,
   547  	tags map[string]string,
   548  ) (storage.FilesystemInfo, error) {
   549  	lxdPool, volumeName, err := parseFilesystemId(filesystemId)
   550  	if err != nil {
   551  		return storage.FilesystemInfo{}, errors.Trace(err)
   552  	}
   553  	volume, eTag, err := s.env.server.GetStoragePoolVolume(lxdPool, storagePoolVolumeType, volumeName)
   554  	if err != nil {
   555  		common.HandleCredentialError(IsAuthorisationFailure, err, callCtx)
   556  		return storage.FilesystemInfo{}, errors.Trace(err)
   557  	}
   558  	if len(volume.UsedBy) > 0 {
   559  		return storage.FilesystemInfo{}, errors.Errorf(
   560  			"filesystem %q is in use by %d containers, cannot import",
   561  			filesystemId, len(volume.UsedBy),
   562  		)
   563  	}
   564  
   565  	// NOTE(axw) not all drivers support specifying a volume size.
   566  	// If we can't find a size config attribute, we have to make
   567  	// up a number since the model will not allow a size of zero.
   568  	// We use the magic number 999GiB to indicate that it's unknown.
   569  	size := uint64(999 * 1024) // 999GiB
   570  	if sizeString := volume.Config["size"]; sizeString != "" {
   571  		n, err := shared.ParseByteSizeString(sizeString)
   572  		if err != nil {
   573  			return storage.FilesystemInfo{}, errors.Annotate(err, "parsing size")
   574  		}
   575  		// ParseByteSizeString returns bytes, we want MiB.
   576  		size = uint64(n / (1024 * 1024))
   577  	}
   578  
   579  	if len(tags) > 0 {
   580  		// Update the volume's user-data with the given tags. This will
   581  		// include updating the model and controller UUIDs, so that the
   582  		// storage is associated with this controller and model.
   583  		if volume.Config == nil {
   584  			volume.Config = make(map[string]string)
   585  		}
   586  		for k, v := range tags {
   587  			volume.Config["user."+k] = v
   588  		}
   589  		if err := s.env.server.UpdateStoragePoolVolume(
   590  			lxdPool, storagePoolVolumeType, volumeName, volume.Writable(), eTag); err != nil {
   591  			common.HandleCredentialError(IsAuthorisationFailure, err, callCtx)
   592  			return storage.FilesystemInfo{}, errors.Annotate(err, "tagging volume")
   593  		}
   594  	}
   595  
   596  	return storage.FilesystemInfo{
   597  		FilesystemId: filesystemId,
   598  		Size:         size,
   599  	}, nil
   600  }