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 }