github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/provider/openstack/cinder.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package openstack 5 6 import ( 7 "math" 8 "net/url" 9 "sync" 10 "time" 11 12 "github.com/juju/errors" 13 "github.com/juju/utils" 14 "gopkg.in/goose.v1/cinder" 15 "gopkg.in/goose.v1/identity" 16 "gopkg.in/goose.v1/nova" 17 18 "github.com/juju/juju/environs/config" 19 "github.com/juju/juju/environs/tags" 20 "github.com/juju/juju/instance" 21 "github.com/juju/juju/storage" 22 ) 23 24 const ( 25 CinderProviderType = storage.ProviderType("cinder") 26 // autoAssignedMountPoint specifies the value to pass in when 27 // you'd like Cinder to automatically assign a mount point. 28 autoAssignedMountPoint = "" 29 30 volumeStatusAvailable = "available" 31 volumeStatusDeleting = "deleting" 32 volumeStatusError = "error" 33 volumeStatusInUse = "in-use" 34 ) 35 36 type cinderProvider struct { 37 newStorageAdapter func(*config.Config) (openstackStorage, error) 38 } 39 40 var _ storage.Provider = (*cinderProvider)(nil) 41 42 var cinderAttempt = utils.AttemptStrategy{ 43 Total: 1 * time.Minute, 44 Delay: 5 * time.Second, 45 } 46 47 // VolumeSource implements storage.Provider. 48 func (p *cinderProvider) VolumeSource(environConfig *config.Config, providerConfig *storage.Config) (storage.VolumeSource, error) { 49 if err := p.ValidateConfig(providerConfig); err != nil { 50 return nil, err 51 } 52 storageAdapter, err := p.newStorageAdapter(environConfig) 53 if err != nil { 54 return nil, err 55 } 56 source := &cinderVolumeSource{ 57 storageAdapter: storageAdapter, 58 envName: environConfig.Name(), 59 modelUUID: environConfig.UUID(), 60 } 61 return source, nil 62 } 63 64 // FilesystemSource implements storage.Provider. 65 func (p *cinderProvider) FilesystemSource(environConfig *config.Config, providerConfig *storage.Config) (storage.FilesystemSource, error) { 66 return nil, errors.NotSupportedf("filesystems") 67 } 68 69 // Supports implements storage.Provider. 70 func (p *cinderProvider) Supports(kind storage.StorageKind) bool { 71 switch kind { 72 case storage.StorageKindBlock: 73 return true 74 } 75 return false 76 } 77 78 // Scope implements storage.Provider. 79 func (s *cinderProvider) Scope() storage.Scope { 80 return storage.ScopeEnviron 81 } 82 83 // ValidateConfig implements storage.Provider. 84 func (p *cinderProvider) ValidateConfig(cfg *storage.Config) error { 85 // TODO(axw) 2015-05-01 #1450737 86 // Reject attempts to create non-persistent volumes. 87 return nil 88 } 89 90 // Dynamic implements storage.Provider. 91 func (p *cinderProvider) Dynamic() bool { 92 return true 93 } 94 95 type cinderVolumeSource struct { 96 storageAdapter openstackStorage 97 envName string // non unique, informational only 98 modelUUID string 99 } 100 101 var _ storage.VolumeSource = (*cinderVolumeSource)(nil) 102 103 // CreateVolumes implements storage.VolumeSource. 104 func (s *cinderVolumeSource) CreateVolumes(args []storage.VolumeParams) ([]storage.CreateVolumesResult, error) { 105 results := make([]storage.CreateVolumesResult, len(args)) 106 for i, arg := range args { 107 volume, err := s.createVolume(arg) 108 if err != nil { 109 results[i].Error = errors.Trace(err) 110 continue 111 } 112 results[i].Volume = volume 113 } 114 return results, nil 115 } 116 117 func (s *cinderVolumeSource) createVolume(arg storage.VolumeParams) (*storage.Volume, error) { 118 var metadata interface{} 119 if len(arg.ResourceTags) > 0 { 120 metadata = arg.ResourceTags 121 } 122 cinderVolume, err := s.storageAdapter.CreateVolume(cinder.CreateVolumeVolumeParams{ 123 // The Cinder documentation incorrectly states the 124 // size parameter is in GB. It is actually GiB. 125 Size: int(math.Ceil(float64(arg.Size / 1024))), 126 Name: resourceName(arg.Tag, s.envName), 127 // TODO(axw) use the AZ of the initially attached machine. 128 AvailabilityZone: "", 129 Metadata: metadata, 130 }) 131 if err != nil { 132 return nil, errors.Trace(err) 133 } 134 135 // The response may (will?) come back before the volume transitions to 136 // "creating", in which case it will not have a size or status. Wait for 137 // the volume to transition, so we can record its actual size. 138 volumeId := cinderVolume.ID 139 cinderVolume, err = s.waitVolume(volumeId, func(v *cinder.Volume) (bool, error) { 140 return v.Status != "", nil 141 }) 142 if err != nil { 143 if err := s.storageAdapter.DeleteVolume(volumeId); err != nil { 144 logger.Warningf("destroying volume %s: %s", volumeId, err) 145 } 146 return nil, errors.Errorf("waiting for volume to be provisioned: %s", err) 147 } 148 logger.Debugf("created volume: %+v", cinderVolume) 149 return &storage.Volume{arg.Tag, cinderToJujuVolumeInfo(cinderVolume)}, nil 150 } 151 152 // ListVolumes is specified on the storage.VolumeSource interface. 153 func (s *cinderVolumeSource) ListVolumes() ([]string, error) { 154 cinderVolumes, err := s.storageAdapter.GetVolumesDetail() 155 if err != nil { 156 return nil, err 157 } 158 volumeIds := make([]string, 0, len(cinderVolumes)) 159 for _, volume := range cinderVolumes { 160 modelUUID, ok := volume.Metadata[tags.JujuModel] 161 if !ok || modelUUID != s.modelUUID { 162 continue 163 } 164 volumeIds = append(volumeIds, cinderToJujuVolumeInfo(&volume).VolumeId) 165 } 166 return volumeIds, nil 167 } 168 169 // DescribeVolumes implements storage.VolumeSource. 170 func (s *cinderVolumeSource) DescribeVolumes(volumeIds []string) ([]storage.DescribeVolumesResult, error) { 171 // In most cases, it is quicker to get all volumes and loop 172 // locally than to make several round-trips to the provider. 173 cinderVolumes, err := s.storageAdapter.GetVolumesDetail() 174 if err != nil { 175 return nil, errors.Trace(err) 176 } 177 volumesById := make(map[string]*cinder.Volume) 178 for i, volume := range cinderVolumes { 179 volumesById[volume.ID] = &cinderVolumes[i] 180 } 181 results := make([]storage.DescribeVolumesResult, len(volumeIds)) 182 for i, volumeId := range volumeIds { 183 cinderVolume, ok := volumesById[volumeId] 184 if !ok { 185 results[i].Error = errors.NotFoundf("volume %q", volumeId) 186 continue 187 } 188 info := cinderToJujuVolumeInfo(cinderVolume) 189 results[i].VolumeInfo = &info 190 } 191 return results, nil 192 } 193 194 // DestroyVolumes implements storage.VolumeSource. 195 func (s *cinderVolumeSource) DestroyVolumes(volumeIds []string) ([]error, error) { 196 var wg sync.WaitGroup 197 wg.Add(len(volumeIds)) 198 results := make([]error, len(volumeIds)) 199 for i, volumeId := range volumeIds { 200 go func(i int, volumeId string) { 201 defer wg.Done() 202 results[i] = s.destroyVolume(volumeId) 203 }(i, volumeId) 204 } 205 wg.Wait() 206 return results, nil 207 } 208 209 func (s *cinderVolumeSource) destroyVolume(volumeId string) error { 210 logger.Debugf("destroying volume %q", volumeId) 211 // Volumes must not be in-use when destroying. A volume may 212 // still be in-use when the instance it is attached to is 213 // in the process of being terminated. 214 var issuedDetach bool 215 volume, err := s.waitVolume(volumeId, func(v *cinder.Volume) (bool, error) { 216 switch v.Status { 217 default: 218 // Not ready for deletion; keep waiting. 219 return false, nil 220 case volumeStatusAvailable, volumeStatusDeleting, volumeStatusError: 221 return true, nil 222 case volumeStatusInUse: 223 // Detach below. 224 break 225 } 226 // Volume is still attached, so detach it. 227 if !issuedDetach { 228 args := make([]storage.VolumeAttachmentParams, len(v.Attachments)) 229 for i, a := range v.Attachments { 230 args[i].VolumeId = volumeId 231 args[i].InstanceId = instance.Id(a.ServerId) 232 } 233 if len(args) > 0 { 234 results, err := s.DetachVolumes(args) 235 if err != nil { 236 return false, errors.Trace(err) 237 } 238 for _, err := range results { 239 if err != nil { 240 return false, errors.Trace(err) 241 } 242 } 243 } 244 issuedDetach = true 245 } 246 return false, nil 247 }) 248 if err != nil { 249 return errors.Trace(err) 250 } 251 if volume.Status == volumeStatusDeleting { 252 // Already being deleted, nothing to do. 253 return nil 254 } 255 if err := s.storageAdapter.DeleteVolume(volumeId); err != nil { 256 return errors.Trace(err) 257 } 258 return nil 259 } 260 261 // ValidateVolumeParams implements storage.VolumeSource. 262 func (s *cinderVolumeSource) ValidateVolumeParams(params storage.VolumeParams) error { 263 return nil 264 } 265 266 // AttachVolumes implements storage.VolumeSource. 267 func (s *cinderVolumeSource) AttachVolumes(args []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) { 268 results := make([]storage.AttachVolumesResult, len(args)) 269 for i, arg := range args { 270 attachment, err := s.attachVolume(arg) 271 if err != nil { 272 results[i].Error = errors.Trace(err) 273 continue 274 } 275 results[i].VolumeAttachment = attachment 276 } 277 return results, nil 278 } 279 280 func (s *cinderVolumeSource) attachVolume(arg storage.VolumeAttachmentParams) (*storage.VolumeAttachment, error) { 281 // Check to see if the volume is already attached. 282 existingAttachments, err := s.storageAdapter.ListVolumeAttachments(string(arg.InstanceId)) 283 if err != nil { 284 return nil, err 285 } 286 novaAttachment := findAttachment(arg.VolumeId, existingAttachments) 287 if novaAttachment == nil { 288 // A volume must be "available" before it can be attached. 289 if _, err := s.waitVolume(arg.VolumeId, func(v *cinder.Volume) (bool, error) { 290 return v.Status == "available", nil 291 }); err != nil { 292 return nil, errors.Annotate(err, "waiting for volume to become available") 293 } 294 novaAttachment, err = s.storageAdapter.AttachVolume( 295 string(arg.InstanceId), 296 arg.VolumeId, 297 autoAssignedMountPoint, 298 ) 299 if err != nil { 300 return nil, err 301 } 302 } 303 return &storage.VolumeAttachment{ 304 arg.Volume, 305 arg.Machine, 306 storage.VolumeAttachmentInfo{ 307 DeviceName: novaAttachment.Device[len("/dev/"):], 308 }, 309 }, nil 310 } 311 312 func (s *cinderVolumeSource) waitVolume( 313 volumeId string, 314 pred func(*cinder.Volume) (bool, error), 315 ) (*cinder.Volume, error) { 316 for a := cinderAttempt.Start(); a.Next(); { 317 volume, err := s.storageAdapter.GetVolume(volumeId) 318 if err != nil { 319 return nil, errors.Annotate(err, "getting volume") 320 } 321 ok, err := pred(volume) 322 if err != nil { 323 return nil, errors.Trace(err) 324 } 325 if ok { 326 return volume, nil 327 } 328 } 329 return nil, errors.New("timed out") 330 } 331 332 // DetachVolumes implements storage.VolumeSource. 333 func (s *cinderVolumeSource) DetachVolumes(args []storage.VolumeAttachmentParams) ([]error, error) { 334 results := make([]error, len(args)) 335 for i, arg := range args { 336 // Check to see if the volume is already detached. 337 attachments, err := s.storageAdapter.ListVolumeAttachments(string(arg.InstanceId)) 338 if err != nil { 339 results[i] = errors.Annotate(err, "listing volume attachments") 340 continue 341 } 342 if err := detachVolume( 343 string(arg.InstanceId), 344 arg.VolumeId, 345 attachments, 346 s.storageAdapter, 347 ); err != nil { 348 results[i] = errors.Annotatef( 349 err, "detaching volume %s from server %s", 350 arg.VolumeId, arg.InstanceId, 351 ) 352 continue 353 } 354 } 355 return results, nil 356 } 357 358 func cinderToJujuVolumeInfo(volume *cinder.Volume) storage.VolumeInfo { 359 return storage.VolumeInfo{ 360 VolumeId: volume.ID, 361 Size: uint64(volume.Size * 1024), 362 Persistent: true, 363 } 364 } 365 366 func detachVolume(instanceId, volumeId string, attachments []nova.VolumeAttachment, storageAdapter openstackStorage) error { 367 // TODO(axw) verify whether we need to do this find step. From looking at the example 368 // responses in the OpenStack docs, the "attachment ID" is always the same as the 369 // volume ID. So we should just be able to issue a blind detach request, and then 370 // ignore errors that indicate the volume is already detached. 371 if findAttachment(volumeId, attachments) == nil { 372 return nil 373 } 374 return storageAdapter.DetachVolume(instanceId, volumeId) 375 } 376 377 func findAttachment(volId string, attachments []nova.VolumeAttachment) *nova.VolumeAttachment { 378 for _, attachment := range attachments { 379 if attachment.VolumeId == volId { 380 return &attachment 381 } 382 } 383 return nil 384 } 385 386 type openstackStorage interface { 387 GetVolume(volumeId string) (*cinder.Volume, error) 388 GetVolumesDetail() ([]cinder.Volume, error) 389 DeleteVolume(volumeId string) error 390 CreateVolume(cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) 391 AttachVolume(serverId, volumeId, mountPoint string) (*nova.VolumeAttachment, error) 392 DetachVolume(serverId, attachmentId string) error 393 ListVolumeAttachments(serverId string) ([]nova.VolumeAttachment, error) 394 } 395 396 type endpointResolver interface { 397 EndpointsForRegion(region string) identity.ServiceURLs 398 } 399 400 func getVolumeEndpointURL(client endpointResolver, region string) (*url.URL, error) { 401 endpointMap := client.EndpointsForRegion(region) 402 // The cinder openstack charm appends 'v2' to the type for the v2 api. 403 endpoint, ok := endpointMap["volumev2"] 404 if !ok { 405 logger.Debugf(`endpoint "volumev2" not found for %q region, trying "volume"`, region) 406 endpoint, ok = endpointMap["volume"] 407 if !ok { 408 return nil, errors.Errorf(`endpoint "volume" not found for %q region`, region) 409 } 410 } 411 return url.Parse(endpoint) 412 } 413 414 func newOpenstackStorageAdapter(environConfig *config.Config) (openstackStorage, error) { 415 ecfg, err := providerInstance.newConfig(environConfig) 416 if err != nil { 417 return nil, errors.Trace(err) 418 } 419 client, err := authClient(ecfg) 420 if err != nil { 421 return nil, errors.Trace(err) 422 } else if err := client.Authenticate(); err != nil { 423 return nil, errors.Trace(err) 424 } 425 426 endpointUrl, err := getVolumeEndpointURL(client, ecfg.region()) 427 if err != nil { 428 return nil, errors.Annotate(err, "getting volume endpoint") 429 } 430 431 return &openstackStorageAdapter{ 432 cinderClient{cinder.Basic(endpointUrl, client.TenantId(), client.Token)}, 433 novaClient{nova.New(client)}, 434 }, nil 435 } 436 437 type openstackStorageAdapter struct { 438 cinderClient 439 novaClient 440 } 441 442 type cinderClient struct { 443 *cinder.Client 444 } 445 446 type novaClient struct { 447 *nova.Client 448 } 449 450 // CreateVolume is part of the openstackStorage interface. 451 func (ga *openstackStorageAdapter) CreateVolume(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) { 452 resp, err := ga.cinderClient.CreateVolume(args) 453 if err != nil { 454 return nil, err 455 } 456 return &resp.Volume, nil 457 } 458 459 // GetVolumesDetail is part of the openstackStorage interface. 460 func (ga *openstackStorageAdapter) GetVolumesDetail() ([]cinder.Volume, error) { 461 resp, err := ga.cinderClient.GetVolumesDetail() 462 if err != nil { 463 return nil, err 464 } 465 return resp.Volumes, nil 466 } 467 468 // GetVolume is part of the openstackStorage interface. 469 func (ga *openstackStorageAdapter) GetVolume(volumeId string) (*cinder.Volume, error) { 470 resp, err := ga.cinderClient.GetVolume(volumeId) 471 if err != nil { 472 return nil, err 473 } 474 return &resp.Volume, nil 475 }