mirror of
https://github.com/ansible/awx.git
synced 2026-03-15 07:57:29 -02:30
423 lines
14 KiB
Python
423 lines
14 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright 2012 Rackspace
|
|
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
from functools import wraps
|
|
import time
|
|
|
|
import six
|
|
|
|
import pyrax
|
|
from pyrax.client import BaseClient
|
|
import pyrax.exceptions as exc
|
|
from pyrax.manager import BaseManager
|
|
from pyrax.resource import BaseResource
|
|
|
|
|
|
MIN_SIZE = 100
|
|
MAX_SIZE = 1024
|
|
RETRY_INTERVAL = 5
|
|
|
|
|
|
def _resolve_id(val):
|
|
"""Takes an object or an ID and returns the ID."""
|
|
return val if isinstance(val, six.string_types) else val.id
|
|
|
|
|
|
def _resolve_name(val):
|
|
"""Takes an object or a name and returns the name."""
|
|
return val if isinstance(val, six.string_types) else val.name
|
|
|
|
|
|
def assure_volume(fnc):
|
|
"""
|
|
Converts a volumeID passed as the volume to a CloudBlockStorageVolume object.
|
|
"""
|
|
@wraps(fnc)
|
|
def _wrapped(self, volume, *args, **kwargs):
|
|
if not isinstance(volume, CloudBlockStorageVolume):
|
|
# Must be the ID
|
|
volume = self._manager.get(volume)
|
|
return fnc(self, volume, *args, **kwargs)
|
|
return _wrapped
|
|
|
|
|
|
def assure_snapshot(fnc):
|
|
"""
|
|
Converts a snapshot ID passed as the snapshot to a CloudBlockStorageSnapshot
|
|
object.
|
|
"""
|
|
@wraps(fnc)
|
|
def _wrapped(self, snapshot, *args, **kwargs):
|
|
if not isinstance(snapshot, CloudBlockStorageSnapshot):
|
|
# Must be the ID
|
|
snapshot = self._snapshot_manager.get(snapshot)
|
|
return fnc(self, snapshot, *args, **kwargs)
|
|
return _wrapped
|
|
|
|
|
|
|
|
class CloudBlockStorageSnapshot(BaseResource):
|
|
"""
|
|
This class represents a Snapshot (copy) of a Block Storage Volume.
|
|
"""
|
|
def delete(self):
|
|
"""
|
|
Adds a check to make sure that the snapshot is able to be deleted.
|
|
"""
|
|
if self.status not in ("available", "error"):
|
|
raise exc.SnapshotNotAvailable("Snapshot must be in 'available' "
|
|
"or 'error' status before deleting. Current status: %s" %
|
|
self.status)
|
|
# When there are more thann one snapshot for a given volume, attempting to
|
|
# delete them all will throw a 409 exception. This will help by retrying
|
|
# such an error once after a RETRY_INTERVAL second delay.
|
|
try:
|
|
super(CloudBlockStorageSnapshot, self).delete()
|
|
except exc.ClientException as e:
|
|
if "Request conflicts with in-progress 'DELETE" in str(e):
|
|
time.sleep(RETRY_INTERVAL)
|
|
# Try again; if it fails, oh, well...
|
|
super(CloudBlockStorageSnapshot, self).delete()
|
|
|
|
|
|
def _get_name(self):
|
|
return self.display_name
|
|
|
|
def _set_name(self, val):
|
|
self.display_name = val
|
|
|
|
name = property(_get_name, _set_name, None,
|
|
"Convenience for referencing the display_name.")
|
|
|
|
def _get_description(self):
|
|
return self.display_description
|
|
|
|
def _set_description(self, val):
|
|
self.display_description = val
|
|
|
|
description = property(_get_description, _set_description, None,
|
|
"Convenience for referencing the display_description.")
|
|
|
|
|
|
class CloudBlockStorageVolumeType(BaseResource):
|
|
"""
|
|
This class represents a Block Storage Volume Type.
|
|
"""
|
|
pass
|
|
|
|
|
|
class CloudBlockStorageVolume(BaseResource):
|
|
"""
|
|
This class represents a Block Storage volume.
|
|
"""
|
|
def __init__(self, *args, **kwargs):
|
|
super(CloudBlockStorageVolume, self).__init__(*args, **kwargs)
|
|
region = self.manager.api.region_name
|
|
self._nova_volumes = pyrax.connect_to_cloudservers(region).volumes
|
|
|
|
|
|
def attach_to_instance(self, instance, mountpoint):
|
|
"""
|
|
Attaches this volume to the cloud server instance at the
|
|
specified mountpoint. This requires a call to the cloud servers
|
|
API; it cannot be done directly.
|
|
"""
|
|
instance_id = _resolve_id(instance)
|
|
try:
|
|
resp = self._nova_volumes.create_server_volume(instance_id,
|
|
self.id, mountpoint)
|
|
except Exception as e:
|
|
raise exc.VolumeAttachmentFailed("%s" % e)
|
|
|
|
|
|
def detach(self):
|
|
"""
|
|
Detaches this volume from any device it may be attached to. If it
|
|
is not attached, nothing happens.
|
|
"""
|
|
attachments = self.attachments
|
|
if not attachments:
|
|
# Not attached; no error needed, just return
|
|
return
|
|
# A volume can only be attached to one device at a time, but for some
|
|
# reason this is a list instead of a singular value
|
|
att = attachments[0]
|
|
instance_id = att["server_id"]
|
|
attachment_id = att["id"]
|
|
try:
|
|
self._nova_volumes.delete_server_volume(instance_id, attachment_id)
|
|
except Exception as e:
|
|
raise exc.VolumeDetachmentFailed("%s" % e)
|
|
|
|
|
|
def delete(self, force=False):
|
|
"""
|
|
Volumes cannot be deleted if either a) they are attached to a device, or
|
|
b) they have any snapshots. This method overrides the base delete()
|
|
method to both better handle these failures, and also to offer a 'force'
|
|
option. When 'force' is True, the volume is detached, and any dependent
|
|
snapshots are deleted before calling the volume's delete.
|
|
"""
|
|
if force:
|
|
self.detach()
|
|
self.delete_all_snapshots()
|
|
try:
|
|
super(CloudBlockStorageVolume, self).delete()
|
|
except exc.VolumeNotAvailable:
|
|
# Notify the user? Record it somewhere?
|
|
# For now, just re-raise
|
|
raise
|
|
|
|
|
|
def create_snapshot(self, name=None, description=None, force=False):
|
|
"""
|
|
Creates a snapshot of this volume, with an optional name and
|
|
description.
|
|
|
|
Normally snapshots will not happen if the volume is attached. To
|
|
override this default behavior, pass force=True.
|
|
"""
|
|
name = name or ""
|
|
description = description or ""
|
|
# Note that passing in non-None values is required for the _create_body
|
|
# method to distinguish between this and the request to create and
|
|
# instance.
|
|
return self.manager.create_snapshot(volume=self, name=name,
|
|
description=description, force=force)
|
|
|
|
|
|
def list_snapshots(self):
|
|
"""
|
|
Returns a list of all snapshots of this volume.
|
|
"""
|
|
return [snap for snap in self.manager.list_snapshots()
|
|
if snap.volume_id == self.id]
|
|
|
|
|
|
def delete_all_snapshots(self):
|
|
"""
|
|
Locates all snapshots of this volume and deletes them.
|
|
"""
|
|
for snap in self.list_snapshots():
|
|
snap.delete()
|
|
|
|
|
|
def _get_name(self):
|
|
return self.display_name
|
|
|
|
def _set_name(self, val):
|
|
self.display_name = val
|
|
|
|
name = property(_get_name, _set_name, None,
|
|
"Convenience for referencing the display_name.")
|
|
|
|
def _get_description(self):
|
|
return self.display_description
|
|
|
|
def _set_description(self, val):
|
|
self.display_description = val
|
|
|
|
description = property(_get_description, _set_description, None,
|
|
"Convenience for referencing the display_description.")
|
|
|
|
|
|
class CloudBlockStorageManager(BaseManager):
|
|
"""
|
|
Manager class for Cloud Block Storage.
|
|
"""
|
|
def _create_body(self, name, size=None, volume_type=None, description=None,
|
|
metadata=None, snapshot_id=None, clone_id=None,
|
|
availability_zone=None):
|
|
"""
|
|
Used to create the dict required to create a new volume
|
|
"""
|
|
if not isinstance(size, (int, long)) or not (
|
|
MIN_SIZE <= size <= MAX_SIZE):
|
|
raise exc.InvalidSize("Volume sizes must be integers between "
|
|
"%s and %s." % (MIN_SIZE, MAX_SIZE))
|
|
if volume_type is None:
|
|
volume_type = "SATA"
|
|
if description is None:
|
|
description = ""
|
|
if metadata is None:
|
|
metadata = {}
|
|
body = {"volume": {
|
|
"size": size,
|
|
"snapshot_id": snapshot_id,
|
|
"source_volid": clone_id,
|
|
"display_name": name,
|
|
"display_description": description,
|
|
"volume_type": volume_type,
|
|
"metadata": metadata,
|
|
"availability_zone": availability_zone,
|
|
}}
|
|
return body
|
|
|
|
|
|
def create(self, *args, **kwargs):
|
|
"""
|
|
Catches errors that may be returned, and raises more informational
|
|
exceptions.
|
|
"""
|
|
try:
|
|
return super(CloudBlockStorageManager, self).create(*args,
|
|
**kwargs)
|
|
except exc.BadRequest as e:
|
|
msg = e.message
|
|
if "Clones currently must be >= original volume size" in msg:
|
|
raise exc.VolumeCloneTooSmall(msg)
|
|
else:
|
|
raise
|
|
|
|
|
|
def list_snapshots(self):
|
|
"""
|
|
Pass-through method to allow the list_snapshots() call to be made
|
|
directly on a volume.
|
|
"""
|
|
return self.api.list_snapshots()
|
|
|
|
|
|
def create_snapshot(self, volume, name, description=None, force=False):
|
|
"""
|
|
Pass-through method to allow the create_snapshot() call to be made
|
|
directly on a volume.
|
|
"""
|
|
return self.api.create_snapshot(volume, name, description=description,
|
|
force=force)
|
|
|
|
|
|
|
|
class CloudBlockStorageSnapshotManager(BaseManager):
|
|
"""
|
|
Manager class for Cloud Block Storage.
|
|
"""
|
|
def _create_body(self, name, description=None, volume=None, force=False):
|
|
"""
|
|
Used to create the dict required to create a new snapshot
|
|
"""
|
|
body = {"snapshot": {
|
|
"display_name": name,
|
|
"display_description": description,
|
|
"volume_id": volume.id,
|
|
"force": str(force).lower(),
|
|
}}
|
|
return body
|
|
|
|
|
|
def create(self, name, volume, description=None, force=False):
|
|
"""
|
|
Adds exception handling to the default create() call.
|
|
"""
|
|
try:
|
|
snap = super(CloudBlockStorageSnapshotManager, self).create(
|
|
name=name, volume=volume, description=description,
|
|
force=force)
|
|
except exc.BadRequest as e:
|
|
msg = str(e)
|
|
if "Invalid volume: must be available" in msg:
|
|
# The volume for the snapshot was attached.
|
|
raise exc.VolumeNotAvailable("Cannot create a snapshot from an "
|
|
"attached volume. Detach the volume before trying "
|
|
"again, or pass 'force=True' to the create_snapshot() "
|
|
"call.")
|
|
else:
|
|
# Some other error
|
|
raise
|
|
except exc.ClientException as e:
|
|
if e.code == 409:
|
|
if "Request conflicts with in-progress" in str(e):
|
|
txt = ("The volume is current creating a snapshot. You "
|
|
"must wait until that completes before attempting "
|
|
"to create an additional snapshot.")
|
|
raise exc.VolumeNotAvailable(txt)
|
|
else:
|
|
raise
|
|
else:
|
|
raise
|
|
return snap
|
|
|
|
|
|
class CloudBlockStorageClient(BaseClient):
|
|
"""
|
|
This is the primary class for interacting with Cloud Block Storage.
|
|
"""
|
|
name = "Cloud Block Storage"
|
|
|
|
def _configure_manager(self):
|
|
"""
|
|
Create the manager to handle the instances, and also another
|
|
to handle flavors.
|
|
"""
|
|
self._manager = CloudBlockStorageManager(self,
|
|
resource_class=CloudBlockStorageVolume, response_key="volume",
|
|
uri_base="volumes")
|
|
self._types_manager = BaseManager(self,
|
|
resource_class=CloudBlockStorageVolumeType,
|
|
response_key="volume_type", uri_base="types")
|
|
self._snapshot_manager = CloudBlockStorageSnapshotManager(self,
|
|
resource_class=CloudBlockStorageSnapshot,
|
|
response_key="snapshot", uri_base="snapshots")
|
|
|
|
|
|
def list_types(self):
|
|
"""Returns a list of all available volume types."""
|
|
return self._types_manager.list()
|
|
|
|
|
|
def list_snapshots(self):
|
|
"""Returns a list of all snapshots."""
|
|
return self._snapshot_manager.list()
|
|
|
|
|
|
@assure_volume
|
|
def attach_to_instance(self, volume, instance, mountpoint):
|
|
"""Attaches the volume to the specified instance at the mountpoint."""
|
|
return volume.attach_to_instance(instance, mountpoint)
|
|
|
|
|
|
@assure_volume
|
|
def detach(self, volume):
|
|
"""Detaches the volume from whatever device it is attached to."""
|
|
return volume.detach()
|
|
|
|
|
|
@assure_volume
|
|
def delete_volume(self, volume, force=False):
|
|
"""Deletes the volume."""
|
|
return volume.delete(force=force)
|
|
|
|
|
|
@assure_volume
|
|
def create_snapshot(self, volume, name=None, description=None, force=False):
|
|
"""
|
|
Creates a snapshot of the volume, with an optional name and description.
|
|
|
|
Normally snapshots will not happen if the volume is attached. To
|
|
override this default behavior, pass force=True.
|
|
"""
|
|
return self._snapshot_manager.create(volume=volume, name=name,
|
|
description=description, force=force)
|
|
|
|
|
|
@assure_snapshot
|
|
def delete_snapshot(self, snapshot):
|
|
"""Deletes the snapshot."""
|
|
return snapshot.delete()
|