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