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 }