github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/storageprovisioner/filesystem_ops.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package storageprovisioner
     5  
     6  import (
     7  	stdcontext "context"
     8  	"path/filepath"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/names/v5"
    12  
    13  	"github.com/juju/juju/core/status"
    14  	environscontext "github.com/juju/juju/environs/context"
    15  	"github.com/juju/juju/rpc/params"
    16  	"github.com/juju/juju/storage"
    17  	"github.com/juju/juju/wrench"
    18  )
    19  
    20  // createFilesystems creates filesystems with the specified parameters.
    21  func createFilesystems(ctx *context, ops map[names.FilesystemTag]*createFilesystemOp) error {
    22  	filesystemParams := make([]storage.FilesystemParams, 0, len(ops))
    23  	for _, op := range ops {
    24  		filesystemParams = append(filesystemParams, op.args)
    25  	}
    26  	paramsBySource, filesystemSources, err := filesystemParamsBySource(
    27  		ctx.config.StorageDir,
    28  		filesystemParams,
    29  		ctx.managedFilesystemSource,
    30  		ctx.config.Registry,
    31  	)
    32  	if err != nil {
    33  		return errors.Trace(err)
    34  	}
    35  	var reschedule []scheduleOp
    36  	var filesystems []storage.Filesystem
    37  	var statuses []params.EntityStatusArgs
    38  	for sourceName, filesystemParams := range paramsBySource {
    39  		ctx.config.Logger.Debugf("creating filesystems: %v", filesystemParams)
    40  		filesystemSource := filesystemSources[sourceName]
    41  		validFilesystemParams, validationErrors := validateFilesystemParams(
    42  			filesystemSource, filesystemParams,
    43  		)
    44  		for i, err := range validationErrors {
    45  			if err == nil {
    46  				continue
    47  			}
    48  			statuses = append(statuses, params.EntityStatusArgs{
    49  				Tag:    filesystemParams[i].Tag.String(),
    50  				Status: status.Error.String(),
    51  				Info:   err.Error(),
    52  			})
    53  			ctx.config.Logger.Debugf(
    54  				"failed to validate parameters for %s: %v",
    55  				names.ReadableString(filesystemParams[i].Tag), err,
    56  			)
    57  		}
    58  		filesystemParams = validFilesystemParams
    59  		if len(filesystemParams) == 0 {
    60  			continue
    61  		}
    62  		results, err := filesystemSource.CreateFilesystems(ctx.config.CloudCallContextFunc(stdcontext.Background()), filesystemParams)
    63  		if err != nil {
    64  			return errors.Annotatef(err, "creating filesystems from source %q", sourceName)
    65  		}
    66  		for i, result := range results {
    67  			statuses = append(statuses, params.EntityStatusArgs{
    68  				Tag:    filesystemParams[i].Tag.String(),
    69  				Status: status.Attaching.String(),
    70  			})
    71  			entityStatus := &statuses[len(statuses)-1]
    72  			if result.Error != nil {
    73  				// Reschedule the filesystem creation.
    74  				reschedule = append(reschedule, ops[filesystemParams[i].Tag])
    75  
    76  				// Note: we keep the status as "pending" to indicate
    77  				// that we will retry. When we distinguish between
    78  				// transient and permanent errors, we will set the
    79  				// status to "error" for permanent errors.
    80  				entityStatus.Status = status.Pending.String()
    81  				entityStatus.Info = result.Error.Error()
    82  				ctx.config.Logger.Debugf(
    83  					"failed to create %s: %v",
    84  					names.ReadableString(filesystemParams[i].Tag),
    85  					result.Error,
    86  				)
    87  				continue
    88  			}
    89  			filesystems = append(filesystems, *result.Filesystem)
    90  		}
    91  	}
    92  	scheduleOperations(ctx, reschedule...)
    93  	setStatus(ctx, statuses)
    94  	if len(filesystems) == 0 {
    95  		return nil
    96  	}
    97  	// TODO(axw) we need to be able to list filesystems in the provider,
    98  	// by environment, so that we can "harvest" them if they're
    99  	// unknown. This will take care of killing filesystems that we fail
   100  	// to record in state.
   101  	errorResults, err := ctx.config.Filesystems.SetFilesystemInfo(filesystemsFromStorage(filesystems))
   102  	if err != nil {
   103  		return errors.Annotate(err, "publishing filesystems to state")
   104  	}
   105  	for i, result := range errorResults {
   106  		if result.Error != nil {
   107  			ctx.config.Logger.Errorf(
   108  				"publishing filesystem %s to state: %v",
   109  				filesystems[i].Tag.Id(),
   110  				result.Error,
   111  			)
   112  		}
   113  	}
   114  	for _, v := range filesystems {
   115  		updateFilesystem(ctx, v)
   116  	}
   117  	return nil
   118  }
   119  
   120  // attachFilesystems creates filesystem attachments with the specified parameters.
   121  func attachFilesystems(ctx *context, ops map[params.MachineStorageId]*attachFilesystemOp) error {
   122  	filesystemAttachmentParams := make([]storage.FilesystemAttachmentParams, 0, len(ops))
   123  	for _, op := range ops {
   124  		args := op.args
   125  		if args.Path == "" {
   126  			args.Path = filepath.Join(ctx.config.StorageDir, args.Filesystem.Id())
   127  		}
   128  		filesystemAttachmentParams = append(filesystemAttachmentParams, args)
   129  	}
   130  	paramsBySource, filesystemSources, err := filesystemAttachmentParamsBySource(
   131  		ctx.config.StorageDir,
   132  		filesystemAttachmentParams,
   133  		ctx.filesystems,
   134  		ctx.managedFilesystemSource,
   135  		ctx.config.Registry,
   136  	)
   137  	if err != nil {
   138  		return errors.Trace(err)
   139  	}
   140  	var reschedule []scheduleOp
   141  	var filesystemAttachments []storage.FilesystemAttachment
   142  	var statuses []params.EntityStatusArgs
   143  	for sourceName, filesystemAttachmentParams := range paramsBySource {
   144  		ctx.config.Logger.Debugf("attaching filesystems: %+v", filesystemAttachmentParams)
   145  		filesystemSource := filesystemSources[sourceName]
   146  		results, err := filesystemSource.AttachFilesystems(ctx.config.CloudCallContextFunc(stdcontext.Background()), filesystemAttachmentParams)
   147  		if err != nil {
   148  			return errors.Annotatef(err, "attaching filesystems from source %q", sourceName)
   149  		}
   150  		for i, result := range results {
   151  			p := filesystemAttachmentParams[i]
   152  			statuses = append(statuses, params.EntityStatusArgs{
   153  				Tag:    p.Filesystem.String(),
   154  				Status: status.Attached.String(),
   155  			})
   156  			entityStatus := &statuses[len(statuses)-1]
   157  			if result.Error != nil {
   158  				// Reschedule the filesystem attachment.
   159  				id := params.MachineStorageId{
   160  					MachineTag:    p.Machine.String(),
   161  					AttachmentTag: p.Filesystem.String(),
   162  				}
   163  				reschedule = append(reschedule, ops[id])
   164  
   165  				// Note: we keep the status as "attaching" to
   166  				// indicate that we will retry. When we distinguish
   167  				// between transient and permanent errors, we will
   168  				// set the status to "error" for permanent errors.
   169  				entityStatus.Status = status.Attaching.String()
   170  				entityStatus.Info = result.Error.Error()
   171  				ctx.config.Logger.Debugf(
   172  					"failed to attach %s to %s: %v",
   173  					names.ReadableString(p.Filesystem),
   174  					names.ReadableString(p.Machine),
   175  					result.Error,
   176  				)
   177  				continue
   178  			}
   179  			filesystemAttachments = append(filesystemAttachments, *result.FilesystemAttachment)
   180  		}
   181  	}
   182  	scheduleOperations(ctx, reschedule...)
   183  	setStatus(ctx, statuses)
   184  	if err := setFilesystemAttachmentInfo(ctx, filesystemAttachments); err != nil {
   185  		return errors.Trace(err)
   186  	}
   187  	return nil
   188  }
   189  
   190  // removeFilesystems destroys or releases filesystems with the specified parameters.
   191  func removeFilesystems(ctx *context, ops map[names.FilesystemTag]*removeFilesystemOp) error {
   192  	tags := make([]names.FilesystemTag, 0, len(ops))
   193  	for tag := range ops {
   194  		tags = append(tags, tag)
   195  	}
   196  	removeFilesystemParams, err := removeFilesystemParams(ctx, tags)
   197  	if err != nil {
   198  		return errors.Trace(err)
   199  	}
   200  	filesystemParams := make([]storage.FilesystemParams, len(tags))
   201  	removeFilesystemParamsByTag := make(map[names.FilesystemTag]params.RemoveFilesystemParams)
   202  	for i, args := range removeFilesystemParams {
   203  		removeFilesystemParamsByTag[tags[i]] = args
   204  		filesystemParams[i] = storage.FilesystemParams{
   205  			Tag:      tags[i],
   206  			Provider: storage.ProviderType(args.Provider),
   207  		}
   208  	}
   209  	paramsBySource, filesystemSources, err := filesystemParamsBySource(
   210  		ctx.config.StorageDir,
   211  		filesystemParams,
   212  		ctx.managedFilesystemSource,
   213  		ctx.config.Registry,
   214  	)
   215  	if err != nil {
   216  		return errors.Trace(err)
   217  	}
   218  	var remove []names.Tag
   219  	var reschedule []scheduleOp
   220  	var statuses []params.EntityStatusArgs
   221  	removeFilesystems := func(tags []names.FilesystemTag, ids []string, f func(environscontext.ProviderCallContext, []string) ([]error, error)) error {
   222  		if len(ids) == 0 {
   223  			return nil
   224  		}
   225  		errs, err := f(ctx.config.CloudCallContextFunc(stdcontext.Background()), ids)
   226  		if err != nil {
   227  			return errors.Trace(err)
   228  		}
   229  		for i, err := range errs {
   230  			tag := tags[i]
   231  			if wrench.IsActive("storageprovisioner", "RemoveFilesystem") {
   232  				err = errors.New("wrench active")
   233  			}
   234  			if err == nil {
   235  				remove = append(remove, tag)
   236  				continue
   237  			}
   238  			// Failed to destroy or release filesystem; reschedule and update status.
   239  			reschedule = append(reschedule, ops[tag])
   240  			statuses = append(statuses, params.EntityStatusArgs{
   241  				Tag:    tag.String(),
   242  				Status: status.Error.String(),
   243  				Info:   errors.Annotate(err, "removing filesystem").Error(),
   244  			})
   245  		}
   246  		return nil
   247  	}
   248  	for sourceName, filesystemParams := range paramsBySource {
   249  		ctx.config.Logger.Debugf("removing filesystems from %q: %v", sourceName, filesystemParams)
   250  		filesystemSource := filesystemSources[sourceName]
   251  		removeTags := make([]names.FilesystemTag, len(filesystemParams))
   252  		removeParams := make([]params.RemoveFilesystemParams, len(filesystemParams))
   253  		for i, args := range filesystemParams {
   254  			removeTags[i] = args.Tag
   255  			removeParams[i] = removeFilesystemParamsByTag[args.Tag]
   256  		}
   257  		destroyTags, destroyIds, releaseTags, releaseIds := partitionRemoveFilesystemParams(removeTags, removeParams)
   258  		if err := removeFilesystems(destroyTags, destroyIds, filesystemSource.DestroyFilesystems); err != nil {
   259  			return errors.Trace(err)
   260  		}
   261  		if err := removeFilesystems(releaseTags, releaseIds, filesystemSource.ReleaseFilesystems); err != nil {
   262  			return errors.Trace(err)
   263  		}
   264  	}
   265  	scheduleOperations(ctx, reschedule...)
   266  	setStatus(ctx, statuses)
   267  	if err := removeEntities(ctx, remove); err != nil {
   268  		return errors.Annotate(err, "removing filesystems from state")
   269  	}
   270  	return nil
   271  }
   272  
   273  func partitionRemoveFilesystemParams(removeTags []names.FilesystemTag, removeParams []params.RemoveFilesystemParams) (
   274  	destroyTags []names.FilesystemTag, destroyIds []string,
   275  	releaseTags []names.FilesystemTag, releaseIds []string,
   276  ) {
   277  	destroyTags = make([]names.FilesystemTag, 0, len(removeParams))
   278  	destroyIds = make([]string, 0, len(removeParams))
   279  	releaseTags = make([]names.FilesystemTag, 0, len(removeParams))
   280  	releaseIds = make([]string, 0, len(removeParams))
   281  	for i, args := range removeParams {
   282  		tag := removeTags[i]
   283  		if args.Destroy {
   284  			destroyTags = append(destroyTags, tag)
   285  			destroyIds = append(destroyIds, args.FilesystemId)
   286  		} else {
   287  			releaseTags = append(releaseTags, tag)
   288  			releaseIds = append(releaseIds, args.FilesystemId)
   289  		}
   290  	}
   291  	return
   292  }
   293  
   294  // detachFilesystems destroys filesystem attachments with the specified parameters.
   295  func detachFilesystems(ctx *context, ops map[params.MachineStorageId]*detachFilesystemOp) error {
   296  	filesystemAttachmentParams := make([]storage.FilesystemAttachmentParams, 0, len(ops))
   297  	for _, op := range ops {
   298  		filesystemAttachmentParams = append(filesystemAttachmentParams, op.args)
   299  	}
   300  	paramsBySource, filesystemSources, err := filesystemAttachmentParamsBySource(
   301  		ctx.config.StorageDir,
   302  		filesystemAttachmentParams,
   303  		ctx.filesystems,
   304  		ctx.managedFilesystemSource,
   305  		ctx.config.Registry,
   306  	)
   307  	if err != nil {
   308  		return errors.Trace(err)
   309  	}
   310  	var reschedule []scheduleOp
   311  	var statuses []params.EntityStatusArgs
   312  	var remove []params.MachineStorageId
   313  	for sourceName, filesystemAttachmentParams := range paramsBySource {
   314  		ctx.config.Logger.Debugf("detaching filesystems: %+v", filesystemAttachmentParams)
   315  		filesystemSource, ok := filesystemSources[sourceName]
   316  		if !ok && ctx.isApplicationKind() {
   317  			continue
   318  		}
   319  		errs, err := filesystemSource.DetachFilesystems(ctx.config.CloudCallContextFunc(stdcontext.Background()), filesystemAttachmentParams)
   320  		if err != nil {
   321  			return errors.Annotatef(err, "detaching filesystems from source %q", sourceName)
   322  		}
   323  		for i, err := range errs {
   324  			p := filesystemAttachmentParams[i]
   325  			statuses = append(statuses, params.EntityStatusArgs{
   326  				Tag: p.Filesystem.String(),
   327  				// TODO(axw) when we support multiple
   328  				// attachment, we'll have to check if
   329  				// there are any other attachments
   330  				// before saying the status "detached".
   331  				Status: status.Detached.String(),
   332  			})
   333  			id := params.MachineStorageId{
   334  				MachineTag:    p.Machine.String(),
   335  				AttachmentTag: p.Filesystem.String(),
   336  			}
   337  			entityStatus := &statuses[len(statuses)-1]
   338  			if wrench.IsActive("storageprovisioner", "DetachFilesystem") {
   339  				err = errors.New("wrench active")
   340  			}
   341  			if err != nil {
   342  				reschedule = append(reschedule, ops[id])
   343  				entityStatus.Status = status.Detaching.String()
   344  				entityStatus.Info = err.Error()
   345  				ctx.config.Logger.Debugf(
   346  					"failed to detach %s from %s: %v",
   347  					names.ReadableString(p.Filesystem),
   348  					names.ReadableString(p.Machine),
   349  					err,
   350  				)
   351  				continue
   352  			}
   353  			remove = append(remove, id)
   354  		}
   355  	}
   356  	scheduleOperations(ctx, reschedule...)
   357  	setStatus(ctx, statuses)
   358  	if err := removeAttachments(ctx, remove); err != nil {
   359  		return errors.Annotate(err, "removing attachments from state")
   360  	}
   361  	for _, id := range remove {
   362  		delete(ctx.filesystemAttachments, id)
   363  	}
   364  	return nil
   365  }
   366  
   367  // filesystemParamsBySource separates the filesystem parameters by filesystem source.
   368  func filesystemParamsBySource(
   369  	baseStorageDir string,
   370  	params []storage.FilesystemParams,
   371  	managedFilesystemSource storage.FilesystemSource,
   372  	registry storage.ProviderRegistry,
   373  ) (map[string][]storage.FilesystemParams, map[string]storage.FilesystemSource, error) {
   374  	// TODO(axw) later we may have multiple instantiations (sources)
   375  	// for a storage provider, e.g. multiple Ceph installations. For
   376  	// now we assume a single source for each provider type, with no
   377  	// configuration.
   378  	filesystemSources := make(map[string]storage.FilesystemSource)
   379  	for _, params := range params {
   380  		sourceName := string(params.Provider)
   381  		if _, ok := filesystemSources[sourceName]; ok {
   382  			continue
   383  		}
   384  		if params.Volume != (names.VolumeTag{}) {
   385  			filesystemSources[sourceName] = managedFilesystemSource
   386  			continue
   387  		}
   388  		filesystemSource, err := filesystemSource(
   389  			baseStorageDir, sourceName, params.Provider, registry,
   390  		)
   391  		// For k8s models, there may be a not found error as there's only
   392  		// one (model) storage provisioner worker which reacts to all storage,
   393  		// even tmpfs or rootfs which is ostensibly handled by a machine storage
   394  		// provisioner worker. There's no such provisoner for k8s but we still
   395  		// process the detach/destroy so the state model can be updated.
   396  		if errors.Cause(err) == errNonDynamic || errors.IsNotFound(err) {
   397  			filesystemSource = nil
   398  		} else if err != nil {
   399  			return nil, nil, errors.Annotate(err, "getting filesystem source")
   400  		}
   401  		filesystemSources[sourceName] = filesystemSource
   402  	}
   403  	paramsBySource := make(map[string][]storage.FilesystemParams)
   404  	for _, param := range params {
   405  		sourceName := string(param.Provider)
   406  		filesystemSource := filesystemSources[sourceName]
   407  		if filesystemSource == nil {
   408  			// Ignore nil filesystem sources; this means that the
   409  			// filesystem should be created by the machine-provisioner.
   410  			continue
   411  		}
   412  		paramsBySource[sourceName] = append(paramsBySource[sourceName], param)
   413  	}
   414  	return paramsBySource, filesystemSources, nil
   415  }
   416  
   417  // validateFilesystemParams validates a collection of filesystem parameters.
   418  func validateFilesystemParams(
   419  	filesystemSource storage.FilesystemSource,
   420  	filesystemParams []storage.FilesystemParams,
   421  ) ([]storage.FilesystemParams, []error) {
   422  	valid := make([]storage.FilesystemParams, 0, len(filesystemParams))
   423  	results := make([]error, len(filesystemParams))
   424  	for i, params := range filesystemParams {
   425  		err := filesystemSource.ValidateFilesystemParams(params)
   426  		if err == nil {
   427  			valid = append(valid, params)
   428  		}
   429  		results[i] = err
   430  	}
   431  	return valid, results
   432  }
   433  
   434  // filesystemAttachmentParamsBySource separates the filesystem attachment parameters by filesystem source.
   435  func filesystemAttachmentParamsBySource(
   436  	baseStorageDir string,
   437  	filesystemAttachmentParams []storage.FilesystemAttachmentParams,
   438  	filesystems map[names.FilesystemTag]storage.Filesystem,
   439  	managedFilesystemSource storage.FilesystemSource,
   440  	registry storage.ProviderRegistry,
   441  ) (map[string][]storage.FilesystemAttachmentParams, map[string]storage.FilesystemSource, error) {
   442  	// TODO(axw) later we may have multiple instantiations (sources)
   443  	// for a storage provider, e.g. multiple Ceph installations. For
   444  	// now we assume a single source for each provider type, with no
   445  	// configuration.
   446  	filesystemSources := make(map[string]storage.FilesystemSource)
   447  	paramsBySource := make(map[string][]storage.FilesystemAttachmentParams)
   448  	for _, params := range filesystemAttachmentParams {
   449  		sourceName := string(params.Provider)
   450  		paramsBySource[sourceName] = append(paramsBySource[sourceName], params)
   451  		if _, ok := filesystemSources[sourceName]; ok {
   452  			continue
   453  		}
   454  		filesystem, ok := filesystems[params.Filesystem]
   455  		if !ok || filesystem.Volume != (names.VolumeTag{}) {
   456  			filesystemSources[sourceName] = managedFilesystemSource
   457  			continue
   458  		}
   459  		filesystemSource, err := filesystemSource(
   460  			baseStorageDir, sourceName, params.Provider, registry,
   461  		)
   462  		// For k8s models, there may be a not found error as there's only
   463  		// one (model) storage provisioner worker which reacts to all storage,
   464  		// even tmpfs or rootfs which is ostensibly handled by a machine storage
   465  		// provisioner worker. There's no such provisoner for k8s but we still
   466  		// process the detach/destroy so the state model can be updated.
   467  		if err != nil && !errors.IsNotFound(err) {
   468  			return nil, nil, errors.Annotate(err, "getting filesystem source")
   469  		}
   470  		filesystemSources[sourceName] = filesystemSource
   471  	}
   472  	return paramsBySource, filesystemSources, nil
   473  }
   474  
   475  func setFilesystemAttachmentInfo(ctx *context, filesystemAttachments []storage.FilesystemAttachment) error {
   476  	if len(filesystemAttachments) == 0 {
   477  		return nil
   478  	}
   479  	// TODO(axw) we need to be able to list filesystem attachments in the
   480  	// provider, by environment, so that we can "harvest" them if they're
   481  	// unknown. This will take care of killing filesystems that we fail to
   482  	// record in state.
   483  	errorResults, err := ctx.config.Filesystems.SetFilesystemAttachmentInfo(
   484  		filesystemAttachmentsFromStorage(filesystemAttachments),
   485  	)
   486  	if err != nil {
   487  		return errors.Annotate(err, "publishing filesystems to state")
   488  	}
   489  	for i, result := range errorResults {
   490  		if result.Error != nil {
   491  			return errors.Annotatef(
   492  				result.Error, "publishing attachment of %s to %s to state",
   493  				names.ReadableString(filesystemAttachments[i].Filesystem),
   494  				names.ReadableString(filesystemAttachments[i].Machine),
   495  			)
   496  		}
   497  		// Record the filesystem attachment in the context.
   498  		id := params.MachineStorageId{
   499  			MachineTag:    filesystemAttachments[i].Machine.String(),
   500  			AttachmentTag: filesystemAttachments[i].Filesystem.String(),
   501  		}
   502  		ctx.filesystemAttachments[id] = filesystemAttachments[i]
   503  		removePendingFilesystemAttachment(ctx, id)
   504  	}
   505  	return nil
   506  }
   507  
   508  func filesystemsFromStorage(in []storage.Filesystem) []params.Filesystem {
   509  	out := make([]params.Filesystem, len(in))
   510  	for i, f := range in {
   511  		paramsFilesystem := params.Filesystem{
   512  			f.Tag.String(),
   513  			"",
   514  			params.FilesystemInfo{
   515  				f.FilesystemId,
   516  				"", // pool
   517  				f.Size,
   518  			},
   519  		}
   520  		if f.Volume != (names.VolumeTag{}) {
   521  			paramsFilesystem.VolumeTag = f.Volume.String()
   522  		}
   523  		out[i] = paramsFilesystem
   524  	}
   525  	return out
   526  }
   527  
   528  func filesystemAttachmentsFromStorage(in []storage.FilesystemAttachment) []params.FilesystemAttachment {
   529  	out := make([]params.FilesystemAttachment, len(in))
   530  	for i, f := range in {
   531  		out[i] = params.FilesystemAttachment{
   532  			f.Filesystem.String(),
   533  			f.Machine.String(),
   534  			params.FilesystemAttachmentInfo{
   535  				f.Path,
   536  				f.ReadOnly,
   537  			},
   538  		}
   539  	}
   540  	return out
   541  }
   542  
   543  type createFilesystemOp struct {
   544  	exponentialBackoff
   545  	args storage.FilesystemParams
   546  }
   547  
   548  func (op *createFilesystemOp) key() interface{} {
   549  	return op.args.Tag
   550  }
   551  
   552  type removeFilesystemOp struct {
   553  	exponentialBackoff
   554  	tag names.FilesystemTag
   555  }
   556  
   557  func (op *removeFilesystemOp) key() interface{} {
   558  	return op.tag
   559  }
   560  
   561  type attachFilesystemOp struct {
   562  	exponentialBackoff
   563  	args storage.FilesystemAttachmentParams
   564  }
   565  
   566  func (op *attachFilesystemOp) key() interface{} {
   567  	return params.MachineStorageId{
   568  		MachineTag:    op.args.Machine.String(),
   569  		AttachmentTag: op.args.Filesystem.String(),
   570  	}
   571  }
   572  
   573  type detachFilesystemOp struct {
   574  	exponentialBackoff
   575  	args storage.FilesystemAttachmentParams
   576  }
   577  
   578  func (op *detachFilesystemOp) key() interface{} {
   579  	return params.MachineStorageId{
   580  		MachineTag:    op.args.Machine.String(),
   581  		AttachmentTag: op.args.Filesystem.String(),
   582  	}
   583  }