blob: 199a7af9414a4c231cf42e2ccda177edaa4dfacb [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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.
"""
OpenStack driver
"""
from libcloud.utils.iso8601 import parse_date
try:
import simplejson as json
except ImportError:
import json
try:
from lxml import etree as ET
except ImportError:
from xml.etree import ElementTree as ET
import warnings
import base64
from libcloud.utils.py3 import httplib
from libcloud.utils.py3 import b
from libcloud.utils.py3 import next
from libcloud.utils.py3 import urlparse
from libcloud.common.openstack import OpenStackBaseConnection
from libcloud.common.openstack import OpenStackDriverMixin
from libcloud.common.openstack import OpenStackException
from libcloud.common.openstack import OpenStackResponse
from libcloud.utils.networking import is_public_subnet
from libcloud.compute.base import NodeSize, NodeImage
from libcloud.compute.base import (NodeDriver, Node, NodeLocation,
StorageVolume, VolumeSnapshot)
from libcloud.compute.base import KeyPair
from libcloud.compute.types import NodeState, StorageVolumeState, Provider, \
VolumeSnapshotState
from libcloud.pricing import get_size_price
from libcloud.utils.xml import findall
__all__ = [
'OpenStack_1_0_Response',
'OpenStack_1_0_Connection',
'OpenStack_1_0_NodeDriver',
'OpenStack_1_0_SharedIpGroup',
'OpenStack_1_0_NodeIpAddresses',
'OpenStack_1_1_Response',
'OpenStack_1_1_Connection',
'OpenStack_1_1_NodeDriver',
'OpenStack_1_1_FloatingIpPool',
'OpenStack_1_1_FloatingIpAddress',
'OpenStackNodeDriver'
]
ATOM_NAMESPACE = "http://www.w3.org/2005/Atom"
DEFAULT_API_VERSION = '1.1'
class OpenStackComputeConnection(OpenStackBaseConnection):
# default config for http://devstack.org/
service_type = 'compute'
service_name = 'nova'
service_region = 'RegionOne'
class OpenStackNodeDriver(NodeDriver, OpenStackDriverMixin):
"""
Base OpenStack node driver. Should not be used directly.
"""
api_name = 'openstack'
name = 'OpenStack'
website = 'http://openstack.org/'
NODE_STATE_MAP = {
'BUILD': NodeState.PENDING,
'REBUILD': NodeState.PENDING,
'ACTIVE': NodeState.RUNNING,
'SUSPENDED': NodeState.STOPPED,
'SHUTOFF': NodeState.STOPPED,
'DELETED': NodeState.TERMINATED,
'QUEUE_RESIZE': NodeState.PENDING,
'PREP_RESIZE': NodeState.PENDING,
'VERIFY_RESIZE': NodeState.RUNNING,
'PASSWORD': NodeState.PENDING,
'RESCUE': NodeState.PENDING,
'REBOOT': NodeState.REBOOTING,
'HARD_REBOOT': NodeState.REBOOTING,
'SHARE_IP': NodeState.PENDING,
'SHARE_IP_NO_CONFIG': NodeState.PENDING,
'DELETE_IP': NodeState.PENDING,
'ERROR': NodeState.ERROR,
'UNKNOWN': NodeState.UNKNOWN
}
# http://developer.openstack.org/api-ref-blockstorage-v2.html#volumes-v2
VOLUME_STATE_MAP = {
'creating': StorageVolumeState.CREATING,
'available': StorageVolumeState.AVAILABLE,
'attaching': StorageVolumeState.ATTACHING,
'in-use': StorageVolumeState.INUSE,
'deleting': StorageVolumeState.DELETING,
'error': StorageVolumeState.ERROR,
'error_deleting': StorageVolumeState.ERROR,
'backing-up': StorageVolumeState.BACKUP,
'restoring-backup': StorageVolumeState.BACKUP,
'error_restoring': StorageVolumeState.ERROR,
'error_extending': StorageVolumeState.ERROR,
}
# http://developer.openstack.org/api-ref-blockstorage-v2.html#ext-backups-v2
SNAPSHOT_STATE_MAP = {
'creating': VolumeSnapshotState.CREATING,
'available': VolumeSnapshotState.AVAILABLE,
'deleting': VolumeSnapshotState.DELETING,
'error': VolumeSnapshotState.ERROR,
'restoring': VolumeSnapshotState.RESTORING,
'error_restoring': VolumeSnapshotState.ERROR
}
def __new__(cls, key, secret=None, secure=True, host=None, port=None,
api_version=DEFAULT_API_VERSION, **kwargs):
if cls is OpenStackNodeDriver:
if api_version == '1.0':
cls = OpenStack_1_0_NodeDriver
elif api_version == '1.1':
cls = OpenStack_1_1_NodeDriver
else:
raise NotImplementedError(
"No OpenStackNodeDriver found for API version %s" %
(api_version))
return super(OpenStackNodeDriver, cls).__new__(cls)
def __init__(self, *args, **kwargs):
OpenStackDriverMixin.__init__(self, **kwargs)
super(OpenStackNodeDriver, self).__init__(*args, **kwargs)
def destroy_node(self, node):
uri = '/servers/%s' % (node.id)
resp = self.connection.request(uri, method='DELETE')
# The OpenStack and Rackspace documentation both say this API will
# return a 204, but in-fact, everyone everywhere agrees it actually
# returns a 202, so we are going to accept either, and someday,
# someone will fix either the implementation or the documentation to
# agree.
return resp.status in (httplib.NO_CONTENT, httplib.ACCEPTED)
def reboot_node(self, node):
return self._reboot_node(node, reboot_type='HARD')
def list_nodes(self, ex_all_tenants=False):
"""
List the nodes in a tenant
:param ex_all_tenants: List nodes for all the tenants. Note: Your user
must have admin privileges for this
functionality to work.
:type ex_all_tenants: ``bool``
"""
params = {}
if ex_all_tenants:
params = {'all_tenants': 1}
return self._to_nodes(
self.connection.request('/servers/detail', params=params).object)
def create_volume(self, size, name, location=None, snapshot=None,
ex_volume_type=None):
"""
Create a new volume.
:param size: Size of volume in gigabytes (required)
:type size: ``int``
:param name: Name of the volume to be created
:type name: ``str``
:param location: Which data center to create a volume in. If
empty, undefined behavior will be selected.
(optional)
:type location: :class:`.NodeLocation`
:param snapshot: Snapshot from which to create the new
volume. (optional)
:type snapshot: :class:`.VolumeSnapshot`
:param ex_volume_type: What kind of volume to create.
(optional)
:type ex_volume_type: ``str``
:return: The newly created volume.
:rtype: :class:`StorageVolume`
"""
volume = {
'display_name': name,
'display_description': name,
'size': size,
'volume_type': ex_volume_type,
'metadata': {
'contents': name,
},
'availability_zone': location
}
if snapshot:
volume['snapshot_id'] = snapshot.id
resp = self.connection.request('/os-volumes',
method='POST',
data={'volume': volume})
return self._to_volume(resp.object)
def destroy_volume(self, volume):
return self.connection.request('/os-volumes/%s' % volume.id,
method='DELETE').success()
def attach_volume(self, node, volume, device="auto"):
# when "auto" or None is provided for device, openstack will let
# the guest OS pick the next available device (fi. /dev/vdb)
return self.connection.request(
'/servers/%s/os-volume_attachments' % node.id,
method='POST',
data={
'volumeAttachment': {
'volumeId': volume.id,
'device': device,
}
}).success()
def detach_volume(self, volume, ex_node=None):
# when ex_node is not provided, volume is detached from all nodes
failed_nodes = []
for attachment in volume.extra['attachments']:
if not ex_node or ex_node.id == attachment['serverId']:
response = self.connection.request(
'/servers/%s/os-volume_attachments/%s' %
(attachment['serverId'], attachment['id']),
method='DELETE')
if not response.success():
failed_nodes.append(attachment['serverId'])
if failed_nodes:
raise OpenStackException(
'detach_volume failed for nodes with id: %s' %
', '.join(failed_nodes), 500, self
)
return True
def list_volumes(self):
return self._to_volumes(
self.connection.request('/os-volumes').object)
def ex_get_volume(self, volumeId):
return self._to_volume(
self.connection.request('/os-volumes/%s' % volumeId).object)
def list_images(self, location=None, ex_only_active=True):
"""
Lists all active images
@inherits: :class:`NodeDriver.list_images`
:param ex_only_active: True if list only active
:type ex_only_active: ``bool``
"""
return self._to_images(
self.connection.request('/images/detail').object, ex_only_active)
def get_image(self, image_id):
"""
Get an image based on an image_id
@inherits: :class:`NodeDriver.get_image`
:param image_id: Image identifier
:type image_id: ``str``
:return: A NodeImage object
:rtype: :class:`NodeImage`
"""
return self._to_image(self.connection.request(
'/images/%s' % (image_id,)).object['image'])
def list_sizes(self, location=None):
return self._to_sizes(
self.connection.request('/flavors/detail').object)
def list_locations(self):
return [NodeLocation(0, '', '', self)]
def _ex_connection_class_kwargs(self):
return self.openstack_connection_kwargs()
def ex_get_node_details(self, node_id):
"""
Lists details of the specified server.
:param node_id: ID of the node which should be used
:type node_id: ``str``
:rtype: :class:`Node`
"""
# @TODO: Remove this if in 0.6
if isinstance(node_id, Node):
node_id = node_id.id
uri = '/servers/%s' % (node_id)
resp = self.connection.request(uri, method='GET')
if resp.status == httplib.NOT_FOUND:
return None
return self._to_node_from_obj(resp.object)
def ex_soft_reboot_node(self, node):
"""
Soft reboots the specified server
:param node: node
:type node: :class:`Node`
:rtype: ``bool``
"""
return self._reboot_node(node, reboot_type='SOFT')
def ex_hard_reboot_node(self, node):
"""
Hard reboots the specified server
:param node: node
:type node: :class:`Node`
:rtype: ``bool``
"""
return self._reboot_node(node, reboot_type='HARD')
class OpenStackNodeSize(NodeSize):
"""
NodeSize class for the OpenStack.org driver.
Following the example of OpenNebula.org driver
and following guidelines:
https://issues.apache.org/jira/browse/LIBCLOUD-119
"""
def __init__(self, id, name, ram, disk, bandwidth, price, driver,
vcpus=None, ephemeral_disk=None, swap=None, extra=None):
super(OpenStackNodeSize, self).__init__(id=id, name=name, ram=ram,
disk=disk,
bandwidth=bandwidth,
price=price, driver=driver)
self.vcpus = vcpus
self.ephemeral_disk = ephemeral_disk
self.swap = swap
self.extra = extra
def __repr__(self):
return (('<OpenStackNodeSize: id=%s, name=%s, ram=%s, disk=%s, '
'bandwidth=%s, price=%s, driver=%s, vcpus=%s, ...>')
% (self.id, self.name, self.ram, self.disk, self.bandwidth,
self.price, self.driver.name, self.vcpus))
class OpenStack_1_0_Response(OpenStackResponse):
def __init__(self, *args, **kwargs):
# done because of a circular reference from
# NodeDriver -> Connection -> Response
self.node_driver = OpenStack_1_0_NodeDriver
super(OpenStack_1_0_Response, self).__init__(*args, **kwargs)
class OpenStack_1_0_Connection(OpenStackComputeConnection):
responseCls = OpenStack_1_0_Response
default_content_type = 'application/xml; charset=UTF-8'
accept_format = 'application/xml'
XML_NAMESPACE = 'http://docs.rackspacecloud.com/servers/api/v1.0'
class OpenStack_1_0_NodeDriver(OpenStackNodeDriver):
"""
OpenStack node driver.
Extra node attributes:
- password: root password, available after create.
- hostId: represents the host your cloud server runs on
- imageId: id of image
- flavorId: id of flavor
"""
connectionCls = OpenStack_1_0_Connection
type = Provider.OPENSTACK
features = {'create_node': ['generates_password']}
def __init__(self, *args, **kwargs):
self._ex_force_api_version = str(kwargs.pop('ex_force_api_version',
None))
self.XML_NAMESPACE = self.connectionCls.XML_NAMESPACE
super(OpenStack_1_0_NodeDriver, self).__init__(*args, **kwargs)
def _to_images(self, object, ex_only_active):
images = []
for image in findall(object, 'image', self.XML_NAMESPACE):
if ex_only_active and image.get('status') != 'ACTIVE':
continue
images.append(self._to_image(image))
return images
def _to_image(self, element):
return NodeImage(id=element.get('id'),
name=element.get('name'),
driver=self.connection.driver,
extra={'updated': element.get('updated'),
'created': element.get('created'),
'status': element.get('status'),
'serverId': element.get('serverId'),
'progress': element.get('progress'),
'minDisk': element.get('minDisk'),
'minRam': element.get('minRam')
}
)
def _change_password_or_name(self, node, name=None, password=None):
uri = '/servers/%s' % (node.id)
if not name:
name = node.name
body = {'xmlns': self.XML_NAMESPACE,
'name': name}
if password is not None:
body['adminPass'] = password
server_elm = ET.Element('server', body)
resp = self.connection.request(
uri, method='PUT', data=ET.tostring(server_elm))
if resp.status == httplib.NO_CONTENT and password is not None:
node.extra['password'] = password
return resp.status == httplib.NO_CONTENT
def create_node(self, **kwargs):
"""
Create a new node
@inherits: :class:`NodeDriver.create_node`
:keyword ex_metadata: Key/Value metadata to associate with a node
:type ex_metadata: ``dict``
:keyword ex_files: File Path => File contents to create on
the node
:type ex_files: ``dict``
:keyword ex_shared_ip_group_id: The server is launched into
that shared IP group
:type ex_shared_ip_group_id: ``str``
"""
name = kwargs['name']
image = kwargs['image']
size = kwargs['size']
attributes = {'xmlns': self.XML_NAMESPACE,
'name': name,
'imageId': str(image.id),
'flavorId': str(size.id)}
if 'ex_shared_ip_group' in kwargs:
# Deprecate this. Be explicit and call the variable
# ex_shared_ip_group_id since user needs to pass in the id, not the
# name.
warnings.warn('ex_shared_ip_group argument is deprecated.'
' Please use ex_shared_ip_group_id')
if 'ex_shared_ip_group_id' in kwargs:
shared_ip_group_id = kwargs['ex_shared_ip_group_id']
attributes['sharedIpGroupId'] = shared_ip_group_id
server_elm = ET.Element('server', attributes)
metadata_elm = self._metadata_to_xml(kwargs.get("ex_metadata", {}))
if metadata_elm:
server_elm.append(metadata_elm)
files_elm = self._files_to_xml(kwargs.get("ex_files", {}))
if files_elm:
server_elm.append(files_elm)
resp = self.connection.request("/servers",
method='POST',
data=ET.tostring(server_elm))
return self._to_node(resp.object)
def ex_set_password(self, node, password):
"""
Sets the Node's root password.
This will reboot the instance to complete the operation.
:class:`Node.extra['password']` will be set to the new value if the
operation was successful.
:param node: node to set password
:type node: :class:`Node`
:param password: new password.
:type password: ``str``
:rtype: ``bool``
"""
return self._change_password_or_name(node, password=password)
def ex_set_server_name(self, node, name):
"""
Sets the Node's name.
This will reboot the instance to complete the operation.
:param node: node to set name
:type node: :class:`Node`
:param name: new name
:type name: ``str``
:rtype: ``bool``
"""
return self._change_password_or_name(node, name=name)
def ex_resize(self, node, size):
"""
Change an existing server flavor / scale the server up or down.
:param node: node to resize.
:type node: :class:`Node`
:param size: new size.
:type size: :class:`NodeSize`
:rtype: ``bool``
"""
elm = ET.Element(
'resize',
{'xmlns': self.XML_NAMESPACE,
'flavorId': str(size.id)}
)
resp = self.connection.request("/servers/%s/action" % (node.id),
method='POST',
data=ET.tostring(elm))
return resp.status == httplib.ACCEPTED
def ex_confirm_resize(self, node):
"""
Confirm a resize request which is currently in progress. If a resize
request is not explicitly confirmed or reverted it's automatically
confirmed after 24 hours.
For more info refer to the API documentation: http://goo.gl/zjFI1
:param node: node for which the resize request will be confirmed.
:type node: :class:`Node`
:rtype: ``bool``
"""
elm = ET.Element(
'confirmResize',
{'xmlns': self.XML_NAMESPACE},
)
resp = self.connection.request("/servers/%s/action" % (node.id),
method='POST',
data=ET.tostring(elm))
return resp.status == httplib.NO_CONTENT
def ex_revert_resize(self, node):
"""
Revert a resize request which is currently in progress.
All resizes are automatically confirmed after 24 hours if they have
not already been confirmed explicitly or reverted.
For more info refer to the API documentation: http://goo.gl/AizBu
:param node: node for which the resize request will be reverted.
:type node: :class:`Node`
:rtype: ``bool``
"""
elm = ET.Element(
'revertResize',
{'xmlns': self.XML_NAMESPACE}
)
resp = self.connection.request("/servers/%s/action" % (node.id),
method='POST',
data=ET.tostring(elm))
return resp.status == httplib.NO_CONTENT
def ex_rebuild(self, node_id, image_id):
"""
Rebuilds the specified server.
:param node_id: ID of the node which should be used
:type node_id: ``str``
:param image_id: ID of the image which should be used
:type image_id: ``str``
:rtype: ``bool``
"""
# @TODO: Remove those ifs in 0.6
if isinstance(node_id, Node):
node_id = node_id.id
if isinstance(image_id, NodeImage):
image_id = image_id.id
elm = ET.Element(
'rebuild',
{'xmlns': self.XML_NAMESPACE,
'imageId': image_id}
)
resp = self.connection.request("/servers/%s/action" % node_id,
method='POST',
data=ET.tostring(elm))
return resp.status == httplib.ACCEPTED
def ex_create_ip_group(self, group_name, node_id=None):
"""
Creates a shared IP group.
:param group_name: group name which should be used
:type group_name: ``str``
:param node_id: ID of the node which should be used
:type node_id: ``str``
:rtype: ``bool``
"""
# @TODO: Remove this if in 0.6
if isinstance(node_id, Node):
node_id = node_id.id
group_elm = ET.Element(
'sharedIpGroup',
{'xmlns': self.XML_NAMESPACE,
'name': group_name}
)
if node_id:
ET.SubElement(
group_elm,
'server',
{'id': node_id}
)
resp = self.connection.request('/shared_ip_groups',
method='POST',
data=ET.tostring(group_elm))
return self._to_shared_ip_group(resp.object)
def ex_list_ip_groups(self, details=False):
"""
Lists IDs and names for shared IP groups.
If details lists all details for shared IP groups.
:param details: True if details is required
:type details: ``bool``
:rtype: ``list`` of :class:`OpenStack_1_0_SharedIpGroup`
"""
uri = '/shared_ip_groups/detail' if details else '/shared_ip_groups'
resp = self.connection.request(uri,
method='GET')
groups = findall(resp.object, 'sharedIpGroup',
self.XML_NAMESPACE)
return [self._to_shared_ip_group(el) for el in groups]
def ex_delete_ip_group(self, group_id):
"""
Deletes the specified shared IP group.
:param group_id: group id which should be used
:type group_id: ``str``
:rtype: ``bool``
"""
uri = '/shared_ip_groups/%s' % group_id
resp = self.connection.request(uri, method='DELETE')
return resp.status == httplib.NO_CONTENT
def ex_share_ip(self, group_id, node_id, ip, configure_node=True):
"""
Shares an IP address to the specified server.
:param group_id: group id which should be used
:type group_id: ``str``
:param node_id: ID of the node which should be used
:type node_id: ``str``
:param ip: ip which should be used
:type ip: ``str``
:param configure_node: configure node
:type configure_node: ``bool``
:rtype: ``bool``
"""
# @TODO: Remove this if in 0.6
if isinstance(node_id, Node):
node_id = node_id.id
if configure_node:
str_configure = 'true'
else:
str_configure = 'false'
elm = ET.Element(
'shareIp',
{'xmlns': self.XML_NAMESPACE,
'sharedIpGroupId': group_id,
'configureServer': str_configure},
)
uri = '/servers/%s/ips/public/%s' % (node_id, ip)
resp = self.connection.request(uri,
method='PUT',
data=ET.tostring(elm))
return resp.status == httplib.ACCEPTED
def ex_unshare_ip(self, node_id, ip):
"""
Removes a shared IP address from the specified server.
:param node_id: ID of the node which should be used
:type node_id: ``str``
:param ip: ip which should be used
:type ip: ``str``
:rtype: ``bool``
"""
# @TODO: Remove this if in 0.6
if isinstance(node_id, Node):
node_id = node_id.id
uri = '/servers/%s/ips/public/%s' % (node_id, ip)
resp = self.connection.request(uri,
method='DELETE')
return resp.status == httplib.ACCEPTED
def ex_list_ip_addresses(self, node_id):
"""
List all server addresses.
:param node_id: ID of the node which should be used
:type node_id: ``str``
:rtype: :class:`OpenStack_1_0_NodeIpAddresses`
"""
# @TODO: Remove this if in 0.6
if isinstance(node_id, Node):
node_id = node_id.id
uri = '/servers/%s/ips' % node_id
resp = self.connection.request(uri,
method='GET')
return self._to_ip_addresses(resp.object)
def _metadata_to_xml(self, metadata):
if len(metadata) == 0:
return None
metadata_elm = ET.Element('metadata')
for k, v in list(metadata.items()):
meta_elm = ET.SubElement(metadata_elm, 'meta', {'key': str(k)})
meta_elm.text = str(v)
return metadata_elm
def _files_to_xml(self, files):
if len(files) == 0:
return None
personality_elm = ET.Element('personality')
for k, v in list(files.items()):
file_elm = ET.SubElement(personality_elm,
'file',
{'path': str(k)})
file_elm.text = base64.b64encode(b(v))
return personality_elm
def _reboot_node(self, node, reboot_type='SOFT'):
resp = self._node_action(node, ['reboot', ('type', reboot_type)])
return resp.status == httplib.ACCEPTED
def _node_action(self, node, body):
if isinstance(body, list):
attr = ' '.join(['%s="%s"' % (item[0], item[1])
for item in body[1:]])
body = '<%s xmlns="%s" %s/>' % (body[0], self.XML_NAMESPACE, attr)
uri = '/servers/%s/action' % (node.id)
resp = self.connection.request(uri, method='POST', data=body)
return resp
def _to_nodes(self, object):
node_elements = findall(object, 'server', self.XML_NAMESPACE)
return [self._to_node(el) for el in node_elements]
def _to_node_from_obj(self, obj):
return self._to_node(findall(obj, 'server', self.XML_NAMESPACE)[0])
def _to_node(self, el):
def get_ips(el):
return [ip.get('addr') for ip in el]
def get_meta_dict(el):
d = {}
for meta in el:
d[meta.get('key')] = meta.text
return d
public_ip = get_ips(findall(el, 'addresses/public/ip',
self.XML_NAMESPACE))
private_ip = get_ips(findall(el, 'addresses/private/ip',
self.XML_NAMESPACE))
metadata = get_meta_dict(findall(el, 'metadata/meta',
self.XML_NAMESPACE))
n = Node(id=el.get('id'),
name=el.get('name'),
state=self.NODE_STATE_MAP.get(
el.get('status'), NodeState.UNKNOWN),
public_ips=public_ip,
private_ips=private_ip,
driver=self.connection.driver,
extra={
'password': el.get('adminPass'),
'hostId': el.get('hostId'),
'imageId': el.get('imageId'),
'flavorId': el.get('flavorId'),
'uri': "https://%s%s/servers/%s" % (
self.connection.host,
self.connection.request_path, el.get('id')),
'service_name': self.connection.get_service_name(),
'metadata': metadata})
return n
def _to_sizes(self, object):
elements = findall(object, 'flavor', self.XML_NAMESPACE)
return [self._to_size(el) for el in elements]
def _to_size(self, el):
vcpus = int(el.get('vcpus')) if el.get('vcpus', None) else None
return OpenStackNodeSize(id=el.get('id'),
name=el.get('name'),
ram=int(el.get('ram')),
disk=int(el.get('disk')),
# XXX: needs hardcode
vcpus=vcpus,
bandwidth=None,
# Hardcoded
price=self._get_size_price(el.get('id')),
driver=self.connection.driver)
def ex_limits(self):
"""
Extra call to get account's limits, such as
rates (for example amount of POST requests per day)
and absolute limits like total amount of available
RAM to be used by servers.
:return: dict with keys 'rate' and 'absolute'
:rtype: ``dict``
"""
def _to_rate(el):
rate = {}
for item in list(el.items()):
rate[item[0]] = item[1]
return rate
def _to_absolute(el):
return {el.get('name'): el.get('value')}
limits = self.connection.request("/limits").object
rate = [_to_rate(el) for el in findall(limits, 'rate/limit',
self.XML_NAMESPACE)]
absolute = {}
for item in findall(limits, 'absolute/limit',
self.XML_NAMESPACE):
absolute.update(_to_absolute(item))
return {"rate": rate, "absolute": absolute}
def create_image(self, node, name, description=None, reboot=True):
"""Create an image for node.
@inherits: :class:`NodeDriver.create_image`
:param node: node to use as a base for image
:type node: :class:`Node`
:param name: name for new image
:type name: ``str``
:rtype: :class:`NodeImage`
"""
image_elm = ET.Element(
'image',
{'xmlns': self.XML_NAMESPACE,
'name': name,
'serverId': node.id}
)
return self._to_image(
self.connection.request("/images", method="POST",
data=ET.tostring(image_elm)).object)
def delete_image(self, image):
"""Delete an image for node.
@inherits: :class:`NodeDriver.delete_image`
:param image: the image to be deleted
:type image: :class:`NodeImage`
:rtype: ``bool``
"""
uri = '/images/%s' % image.id
resp = self.connection.request(uri, method='DELETE')
return resp.status == httplib.NO_CONTENT
def _to_shared_ip_group(self, el):
servers_el = findall(el, 'servers', self.XML_NAMESPACE)
if servers_el:
servers = [s.get('id')
for s in findall(servers_el[0], 'server',
self.XML_NAMESPACE)]
else:
servers = None
return OpenStack_1_0_SharedIpGroup(id=el.get('id'),
name=el.get('name'),
servers=servers)
def _to_ip_addresses(self, el):
public_ips = [ip.get('addr') for ip in findall(
findall(el, 'public', self.XML_NAMESPACE)[0],
'ip', self.XML_NAMESPACE)]
private_ips = [ip.get('addr') for ip in findall(
findall(el, 'private', self.XML_NAMESPACE)[0],
'ip', self.XML_NAMESPACE)]
return OpenStack_1_0_NodeIpAddresses(public_ips, private_ips)
def _get_size_price(self, size_id):
try:
return get_size_price(driver_type='compute',
driver_name=self.api_name,
size_id=size_id)
except KeyError:
return 0.0
class OpenStack_1_0_SharedIpGroup(object):
"""
Shared IP group info.
"""
def __init__(self, id, name, servers=None):
self.id = str(id)
self.name = name
self.servers = servers
class OpenStack_1_0_NodeIpAddresses(object):
"""
List of public and private IP addresses of a Node.
"""
def __init__(self, public_addresses, private_addresses):
self.public_addresses = public_addresses
self.private_addresses = private_addresses
class OpenStack_1_1_Response(OpenStackResponse):
def __init__(self, *args, **kwargs):
# done because of a circular reference from
# NodeDriver -> Connection -> Response
self.node_driver = OpenStack_1_1_NodeDriver
super(OpenStack_1_1_Response, self).__init__(*args, **kwargs)
class OpenStackNetwork(object):
"""
A Virtual Network.
"""
def __init__(self, id, name, cidr, driver, extra=None):
self.id = str(id)
self.name = name
self.cidr = cidr
self.driver = driver
self.extra = extra or {}
def __repr__(self):
return '<OpenStackNetwork id="%s" name="%s" cidr="%s">' % (self.id,
self.name,
self.cidr,)
class OpenStackSecurityGroup(object):
"""
A Security Group.
"""
def __init__(self, id, tenant_id, name, description, driver, rules=None,
extra=None):
"""
Constructor.
:keyword id: Group id.
:type id: ``str``
:keyword tenant_id: Owner of the security group.
:type tenant_id: ``str``
:keyword name: Human-readable name for the security group. Might
not be unique.
:type name: ``str``
:keyword description: Human-readable description of a security
group.
:type description: ``str``
:keyword rules: Rules associated with this group.
:type rules: ``list`` of
:class:`OpenStackSecurityGroupRule`
:keyword extra: Extra attributes associated with this group.
:type extra: ``dict``
"""
self.id = id
self.tenant_id = tenant_id
self.name = name
self.description = description
self.driver = driver
self.rules = rules or []
self.extra = extra or {}
def __repr__(self):
return ('<OpenStackSecurityGroup id=%s tenant_id=%s name=%s \
description=%s>' % (self.id, self.tenant_id, self.name,
self.description))
class OpenStackSecurityGroupRule(object):
"""
A Rule of a Security Group.
"""
def __init__(self, id, parent_group_id, ip_protocol, from_port, to_port,
driver, ip_range=None, group=None, tenant_id=None,
extra=None):
"""
Constructor.
:keyword id: Rule id.
:type id: ``str``
:keyword parent_group_id: ID of the parent security group.
:type parent_group_id: ``str``
:keyword ip_protocol: IP Protocol (icmp, tcp, udp, etc).
:type ip_protocol: ``str``
:keyword from_port: Port at start of range.
:type from_port: ``int``
:keyword to_port: Port at end of range.
:type to_port: ``int``
:keyword ip_range: CIDR for address range.
:type ip_range: ``str``
:keyword group: Name of a source security group to apply to rule.
:type group: ``str``
:keyword tenant_id: Owner of the security group.
:type tenant_id: ``str``
:keyword extra: Extra attributes associated with this rule.
:type extra: ``dict``
"""
self.id = id
self.parent_group_id = parent_group_id
self.ip_protocol = ip_protocol
self.from_port = from_port
self.to_port = to_port
self.driver = driver
self.ip_range = ''
self.group = {}
if group is None:
self.ip_range = ip_range
else:
self.group = {'name': group, 'tenant_id': tenant_id}
self.tenant_id = tenant_id
self.extra = extra or {}
def __repr__(self):
return ('<OpenStackSecurityGroupRule id=%s parent_group_id=%s \
ip_protocol=%s from_port=%s to_port=%s>' % (self.id,
self.parent_group_id, self.ip_protocol, self.from_port,
self.to_port))
class OpenStackKeyPair(object):
"""
A KeyPair.
"""
def __init__(self, name, fingerprint, public_key, driver, private_key=None,
extra=None):
"""
Constructor.
:keyword name: Name of the KeyPair.
:type name: ``str``
:keyword fingerprint: Fingerprint of the KeyPair
:type fingerprint: ``str``
:keyword public_key: Public key in OpenSSH format.
:type public_key: ``str``
:keyword private_key: Private key in PEM format.
:type private_key: ``str``
:keyword extra: Extra attributes associated with this KeyPair.
:type extra: ``dict``
"""
self.name = name
self.fingerprint = fingerprint
self.public_key = public_key
self.private_key = private_key
self.driver = driver
self.extra = extra or {}
def __repr__(self):
return ('<OpenStackKeyPair name=%s fingerprint=%s public_key=%s ...>'
% (self.name, self.fingerprint, self.public_key))
class OpenStack_1_1_Connection(OpenStackComputeConnection):
responseCls = OpenStack_1_1_Response
accept_format = 'application/json'
default_content_type = 'application/json; charset=UTF-8'
def encode_data(self, data):
return json.dumps(data)
class OpenStack_1_1_NodeDriver(OpenStackNodeDriver):
"""
OpenStack node driver.
"""
connectionCls = OpenStack_1_1_Connection
type = Provider.OPENSTACK
features = {"create_node": ["generates_password"]}
_networks_url_prefix = '/os-networks'
def __init__(self, *args, **kwargs):
self._ex_force_api_version = str(kwargs.pop('ex_force_api_version',
None))
super(OpenStack_1_1_NodeDriver, self).__init__(*args, **kwargs)
def create_node(self, **kwargs):
"""Create a new node
@inherits: :class:`NodeDriver.create_node`
:keyword ex_keyname: The name of the key pair
:type ex_keyname: ``str``
:keyword ex_userdata: String containing user data
see
https://help.ubuntu.com/community/CloudInit
:type ex_userdata: ``str``
:keyword ex_config_drive: Enable config drive
see
http://docs.openstack.org/grizzly/openstack-compute/admin/content/config-drive.html
:type ex_config_drive: ``bool``
:keyword ex_security_groups: List of security groups to assign to
the node
:type ex_security_groups: ``list`` of
:class:`OpenStackSecurityGroup`
:keyword ex_metadata: Key/Value metadata to associate with a node
:type ex_metadata: ``dict``
:keyword ex_files: File Path => File contents to create on
the no de
:type ex_files: ``dict``
:keyword networks: The server is launched into a set of Networks.
:type networks: ``list`` of :class:`OpenStackNetwork`
:keyword ex_disk_config: Name of the disk configuration.
Can be either ``AUTO`` or ``MANUAL``.
:type ex_disk_config: ``str``
:keyword ex_config_drive: If True enables metadata injection in a
server through a configuration drive.
:type ex_config_drive: ``bool``
:keyword ex_admin_pass: The root password for the node
:type ex_admin_pass: ``str``
:keyword ex_availability_zone: Nova availability zone for the node
:type ex_availability_zone: ``str``
"""
server_params = self._create_args_to_params(None, **kwargs)
resp = self.connection.request("/servers",
method='POST',
data={'server': server_params})
create_response = resp.object['server']
server_resp = self.connection.request(
'/servers/%s' % create_response['id'])
server_object = server_resp.object['server']
# adminPass is not always present
# http://docs.openstack.org/essex/openstack-compute/admin/
# content/configuring-compute-API.html#d6e1833
server_object['adminPass'] = create_response.get('adminPass', None)
return self._to_node(server_object)
def _to_images(self, obj, ex_only_active):
images = []
for image in obj['images']:
if ex_only_active and image.get('status') != 'ACTIVE':
continue
images.append(self._to_image(image))
return images
def _to_image(self, api_image):
server = api_image.get('server', {})
return NodeImage(
id=api_image['id'],
name=api_image['name'],
driver=self,
extra=dict(
updated=api_image['updated'],
created=api_image['created'],
status=api_image['status'],
progress=api_image.get('progress'),
metadata=api_image.get('metadata'),
serverId=server.get('id'),
minDisk=api_image.get('minDisk'),
minRam=api_image.get('minRam'),
)
)
def _to_nodes(self, obj):
servers = obj['servers']
return [self._to_node(server) for server in servers]
def _to_volumes(self, obj):
volumes = obj['volumes']
return [self._to_volume(volume) for volume in volumes]
def _to_snapshots(self, obj):
snapshots = obj['snapshots']
return [self._to_snapshot(snapshot) for snapshot in snapshots]
def _to_sizes(self, obj):
flavors = obj['flavors']
return [self._to_size(flavor) for flavor in flavors]
def _create_args_to_params(self, node, **kwargs):
server_params = {
'name': kwargs.get('name'),
'metadata': kwargs.get('ex_metadata', {}),
'personality': self._files_to_personality(kwargs.get("ex_files",
{}))
}
if 'ex_availability_zone' in kwargs:
server_params['availability_zone'] = kwargs['ex_availability_zone']
if 'ex_keyname' in kwargs:
server_params['key_name'] = kwargs['ex_keyname']
if 'ex_userdata' in kwargs:
server_params['user_data'] = base64.b64encode(
b(kwargs['ex_userdata'])).decode('ascii')
if 'ex_config_drive' in kwargs:
server_params['config_drive'] = kwargs['ex_config_drive']
if 'ex_disk_config' in kwargs:
server_params['OS-DCF:diskConfig'] = kwargs['ex_disk_config']
if 'ex_config_drive' in kwargs:
server_params['config_drive'] = str(kwargs['ex_config_drive'])
if 'ex_admin_pass' in kwargs:
server_params['adminPass'] = kwargs['ex_admin_pass']
if 'networks' in kwargs:
networks = kwargs['networks']
networks = [{'uuid': network.id} for network in networks]
server_params['networks'] = networks
if 'ex_security_groups' in kwargs:
server_params['security_groups'] = []
for security_group in kwargs['ex_security_groups']:
name = security_group.name
server_params['security_groups'].append({'name': name})
if 'ex_blockdevicemappings' in kwargs:
server_params['block_device_mapping_v2'] = \
kwargs['ex_blockdevicemappings']
if 'name' in kwargs:
server_params['name'] = kwargs.get('name')
else:
server_params['name'] = node.name
if 'image' in kwargs:
server_params['imageRef'] = kwargs.get('image').id
else:
server_params['imageRef'] = node.extra.get('imageId')
if 'size' in kwargs:
server_params['flavorRef'] = kwargs.get('size').id
else:
server_params['flavorRef'] = node.extra.get('flavorId')
return server_params
def _files_to_personality(self, files):
rv = []
for k, v in list(files.items()):
rv.append({'path': k, 'contents': base64.b64encode(b(v))})
return rv
def _reboot_node(self, node, reboot_type='SOFT'):
resp = self._node_action(node, 'reboot', type=reboot_type)
return resp.status == httplib.ACCEPTED
def ex_set_password(self, node, password):
"""
Changes the administrator password for a specified server.
:param node: Node to rebuild.
:type node: :class:`Node`
:param password: The administrator password.
:type password: ``str``
:rtype: ``bool``
"""
resp = self._node_action(node, 'changePassword', adminPass=password)
node.extra['password'] = password
return resp.status == httplib.ACCEPTED
def ex_rebuild(self, node, image, **kwargs):
"""
Rebuild a Node.
:param node: Node to rebuild.
:type node: :class:`Node`
:param image: New image to use.
:type image: :class:`NodeImage`
:keyword ex_metadata: Key/Value metadata to associate with a node
:type ex_metadata: ``dict``
:keyword ex_files: File Path => File contents to create on
the no de
:type ex_files: ``dict``
:keyword ex_keyname: Name of existing public key to inject into
instance
:type ex_keyname: ``str``
:keyword ex_userdata: String containing user data
see
https://help.ubuntu.com/community/CloudInit
:type ex_userdata: ``str``
:keyword ex_security_groups: List of security groups to assign to
the node
:type ex_security_groups: ``list`` of
:class:`OpenStackSecurityGroup`
:keyword ex_disk_config: Name of the disk configuration.
Can be either ``AUTO`` or ``MANUAL``.
:type ex_disk_config: ``str``
:keyword ex_config_drive: If True enables metadata injection in a
server through a configuration drive.
:type ex_config_drive: ``bool``
:rtype: ``bool``
"""
server_params = self._create_args_to_params(node, image=image,
**kwargs)
resp = self._node_action(node, 'rebuild', **server_params)
return resp.status == httplib.ACCEPTED
def ex_resize(self, node, size):
"""
Change a node size.
:param node: Node to resize.
:type node: :class:`Node`
:type size: :class:`NodeSize`
:param size: New size to use.
:rtype: ``bool``
"""
server_params = self._create_args_to_params(node, size=size)
resp = self._node_action(node, 'resize', **server_params)
return resp.status == httplib.ACCEPTED
def ex_confirm_resize(self, node):
"""
Confirms a pending resize action.
:param node: Node to resize.
:type node: :class:`Node`
:rtype: ``bool``
"""
resp = self._node_action(node, 'confirmResize')
return resp.status == httplib.NO_CONTENT
def ex_revert_resize(self, node):
"""
Cancels and reverts a pending resize action.
:param node: Node to resize.
:type node: :class:`Node`
:rtype: ``bool``
"""
resp = self._node_action(node, 'revertResize')
return resp.status == httplib.ACCEPTED
def create_image(self, node, name, metadata=None):
"""
Creates a new image.
:param node: Node
:type node: :class:`Node`
:param name: The name for the new image.
:type name: ``str``
:param metadata: Key and value pairs for metadata.
:type metadata: ``dict``
:rtype: :class:`NodeImage`
"""
optional_params = {}
if metadata:
optional_params['metadata'] = metadata
resp = self._node_action(node, 'createImage', name=name,
**optional_params)
image_id = self._extract_image_id_from_url(resp.headers['location'])
return self.get_image(image_id=image_id)
def ex_set_server_name(self, node, name):
"""
Sets the Node's name.
:param node: Node
:type node: :class:`Node`
:param name: The name of the server.
:type name: ``str``
:rtype: :class:`Node`
"""
return self._update_node(node, name=name)
def ex_get_metadata(self, node):
"""
Get a Node's metadata.
:param node: Node
:type node: :class:`Node`
:return: Key/Value metadata associated with node.
:rtype: ``dict``
"""
return self.connection.request(
'/servers/%s/metadata' % (node.id,),
method='GET',).object['metadata']
def ex_set_metadata(self, node, metadata):
"""
Sets the Node's metadata.
:param node: Node
:type node: :class:`Node`
:param metadata: Key/Value metadata to associate with a node
:type metadata: ``dict``
:rtype: ``dict``
"""
return self.connection.request(
'/servers/%s/metadata' % (node.id,), method='PUT',
data={'metadata': metadata}
).object['metadata']
def ex_update_node(self, node, **node_updates):
"""
Update the Node's editable attributes. The OpenStack API currently
supports editing name and IPv4/IPv6 access addresses.
The driver currently only supports updating the node name.
:param node: Node
:type node: :class:`Node`
:keyword name: New name for the server
:type name: ``str``
:rtype: :class:`Node`
"""
potential_data = self._create_args_to_params(node, **node_updates)
updates = {'name': potential_data['name']}
return self._update_node(node, **updates)
def _to_networks(self, obj):
networks = obj['networks']
return [self._to_network(network) for network in networks]
def _to_network(self, obj):
return OpenStackNetwork(id=obj['id'],
name=obj['label'],
cidr=obj.get('cidr', None),
driver=self)
def ex_list_networks(self):
"""
Get a list of Networks that are available.
:rtype: ``list`` of :class:`OpenStackNetwork`
"""
response = self.connection.request(self._networks_url_prefix).object
return self._to_networks(response)
def ex_create_network(self, name, cidr):
"""
Create a new Network
:param name: Name of network which should be used
:type name: ``str``
:param cidr: cidr of network which should be used
:type cidr: ``str``
:rtype: :class:`OpenStackNetwork`
"""
data = {'network': {'cidr': cidr, 'label': name}}
response = self.connection.request(self._networks_url_prefix,
method='POST', data=data).object
return self._to_network(response['network'])
def ex_delete_network(self, network):
"""
Get a list of NodeNetorks that are available.
:param network: Network which should be used
:type network: :class:`OpenStackNetwork`
:rtype: ``bool``
"""
resp = self.connection.request('%s/%s' % (self._networks_url_prefix,
network.id),
method='DELETE')
return resp.status == httplib.ACCEPTED
def ex_get_console_output(self, node, length=None):
"""
Get console output
:param node: node
:type node: :class:`Node`
:param length: Optional number of lines to fetch from the
console log
:type length: ``int``
:return: Dictionary with the output
:rtype: ``dict``
"""
data = {
"os-getConsoleOutput": {
"length": length
}
}
resp = self.connection.request('/servers/%s/action' % node.id,
method='POST', data=data).object
return resp
def ex_list_snapshots(self):
return self._to_snapshots(
self.connection.request('/os-snapshots').object)
def list_volume_snapshots(self, volume):
return [snapshot for snapshot in self.ex_list_snapshots()
if snapshot.extra['volume_id'] == volume.id]
def create_volume_snapshot(self, volume, name=None, ex_description=None,
ex_force=True):
"""
Create snapshot from volume
:param volume: Instance of `StorageVolume`
:type volume: `StorageVolume`
:param name: Name of snapshot (optional)
:type name: `str`
:param ex_description: Description of the snapshot (optional)
:type ex_description: `str`
:param ex_force: Specifies if we create a snapshot that is not in
state `available`. For example `in-use`. Defaults
to True. (optional)
:type ex_force: `bool`
:rtype: :class:`VolumeSnapshot`
"""
data = {'snapshot': {'display_name': name,
'display_description': ex_description,
'volume_id': volume.id,
'force': ex_force}}
return self._to_snapshot(self.connection.request('/os-snapshots',
method='POST',
data=data).object)
def destroy_volume_snapshot(self, snapshot):
resp = self.connection.request('/os-snapshots/%s' % snapshot.id,
method='DELETE')
return resp.status == httplib.NO_CONTENT
def ex_create_snapshot(self, volume, name, description=None, force=False):
"""
Create a snapshot based off of a volume.
:param volume: volume
:type volume: :class:`StorageVolume`
:keyword name: New name for the volume snapshot
:type name: ``str``
:keyword description: Description of the snapshot (optional)
:type description: ``str``
:keyword force: Whether to force creation (optional)
:type force: ``bool``
:rtype: :class:`VolumeSnapshot`
"""
warnings.warn('This method has been deprecated in favor of the '
'create_volume_snapshot method')
return self.create_volume_snapshot(volume, name,
ex_description=description,
ex_force=force)
def ex_delete_snapshot(self, snapshot):
"""
Delete a VolumeSnapshot
:param snapshot: snapshot
:type snapshot: :class:`VolumeSnapshot`
:rtype: ``bool``
"""
warnings.warn('This method has been deprecated in favor of the '
'destroy_volume_snapshot method')
return self.destroy_volume_snapshot(snapshot)
def _to_security_group_rules(self, obj):
return [self._to_security_group_rule(security_group_rule) for
security_group_rule in obj]
def _to_security_group_rule(self, obj):
ip_range = group = tenant_id = None
if obj['group'] == {}:
ip_range = obj['ip_range'].get('cidr', None)
else:
group = obj['group'].get('name', None)
tenant_id = obj['group'].get('tenant_id', None)
return OpenStackSecurityGroupRule(
id=obj['id'], parent_group_id=obj['parent_group_id'],
ip_protocol=obj['ip_protocol'], from_port=obj['from_port'],
to_port=obj['to_port'], driver=self, ip_range=ip_range,
group=group, tenant_id=tenant_id)
def _to_security_groups(self, obj):
security_groups = obj['security_groups']
return [self._to_security_group(security_group) for security_group in
security_groups]
def _to_security_group(self, obj):
rules = self._to_security_group_rules(obj.get('rules', []))
return OpenStackSecurityGroup(id=obj['id'],
tenant_id=obj['tenant_id'],
name=obj['name'],
description=obj.get('description', ''),
rules=rules,
driver=self)
def ex_list_security_groups(self):
"""
Get a list of Security Groups that are available.
:rtype: ``list`` of :class:`OpenStackSecurityGroup`
"""
return self._to_security_groups(
self.connection.request('/os-security-groups').object)
def ex_get_node_security_groups(self, node):
"""
Get Security Groups of the specified server.
:rtype: ``list`` of :class:`OpenStackSecurityGroup`
"""
return self._to_security_groups(
self.connection.request('/servers/%s/os-security-groups' %
(node.id)).object)
def ex_create_security_group(self, name, description):
"""
Create a new Security Group
:param name: Name of the new Security Group
:type name: ``str``
:param description: Description of the new Security Group
:type description: ``str``
:rtype: :class:`OpenStackSecurityGroup`
"""
return self._to_security_group(self.connection.request(
'/os-security-groups', method='POST',
data={'security_group': {'name': name, 'description': description}}
).object['security_group'])
def ex_delete_security_group(self, security_group):
"""
Delete a Security Group.
:param security_group: Security Group should be deleted
:type security_group: :class:`OpenStackSecurityGroup`
:rtype: ``bool``
"""
resp = self.connection.request('/os-security-groups/%s' %
(security_group.id),
method='DELETE')
return resp.status in (httplib.NO_CONTENT, httplib.ACCEPTED)
def ex_create_security_group_rule(self, security_group, ip_protocol,
from_port, to_port, cidr=None,
source_security_group=None):
"""
Create a new Rule in a Security Group
:param security_group: Security Group in which to add the rule
:type security_group: :class:`OpenStackSecurityGroup`
:param ip_protocol: Protocol to which this rule applies
Examples: tcp, udp, ...
:type ip_protocol: ``str``
:param from_port: First port of the port range
:type from_port: ``int``
:param to_port: Last port of the port range
:type to_port: ``int``
:param cidr: CIDR notation of the source IP range for this rule
:type cidr: ``str``
:param source_security_group: Existing Security Group to use as the
source (instead of CIDR)
:type source_security_group: L{OpenStackSecurityGroup
:rtype: :class:`OpenStackSecurityGroupRule`
"""
source_security_group_id = None
if type(source_security_group) == OpenStackSecurityGroup:
source_security_group_id = source_security_group.id
return self._to_security_group_rule(self.connection.request(
'/os-security-group-rules', method='POST',
data={'security_group_rule': {
'ip_protocol': ip_protocol,
'from_port': from_port,
'to_port': to_port,
'cidr': cidr,
'group_id': source_security_group_id,
'parent_group_id': security_group.id}}
).object['security_group_rule'])
def ex_delete_security_group_rule(self, rule):
"""
Delete a Rule from a Security Group.
:param rule: Rule should be deleted
:type rule: :class:`OpenStackSecurityGroupRule`
:rtype: ``bool``
"""
resp = self.connection.request('/os-security-group-rules/%s' %
(rule.id), method='DELETE')
return resp.status == httplib.NO_CONTENT
def _to_key_pairs(self, obj):
key_pairs = obj['keypairs']
key_pairs = [self._to_key_pair(key_pair['keypair']) for key_pair in
key_pairs]
return key_pairs
def _to_key_pair(self, obj):
key_pair = KeyPair(name=obj['name'],
fingerprint=obj['fingerprint'],
public_key=obj['public_key'],
private_key=obj.get('private_key', None),
driver=self)
return key_pair
def list_key_pairs(self):
response = self.connection.request('/os-keypairs')
key_pairs = self._to_key_pairs(response.object)
return key_pairs
def get_key_pair(self, name):
self.connection.set_context({'key_pair_name': name})
response = self.connection.request('/os-keypairs/%s' % (name))
key_pair = self._to_key_pair(response.object['keypair'])
return key_pair
def create_key_pair(self, name):
data = {'keypair': {'name': name}}
response = self.connection.request('/os-keypairs', method='POST',
data=data)
key_pair = self._to_key_pair(response.object['keypair'])
return key_pair
def import_key_pair_from_string(self, name, key_material):
data = {'keypair': {'name': name, 'public_key': key_material}}
response = self.connection.request('/os-keypairs', method='POST',
data=data)
key_pair = self._to_key_pair(response.object['keypair'])
return key_pair
def delete_key_pair(self, key_pair):
"""
Delete a KeyPair.
:param keypair: KeyPair to delete
:type keypair: :class:`OpenStackKeyPair`
:rtype: ``bool``
"""
response = self.connection.request('/os-keypairs/%s' % (key_pair.name),
method='DELETE')
return response.status == httplib.ACCEPTED
def ex_list_keypairs(self):
"""
Get a list of KeyPairs that are available.
:rtype: ``list`` of :class:`OpenStackKeyPair`
"""
warnings.warn('This method has been deprecated in favor of '
'list_key_pairs method')
return self.list_key_pairs()
def ex_create_keypair(self, name):
"""
Create a new KeyPair
:param name: Name of the new KeyPair
:type name: ``str``
:rtype: :class:`OpenStackKeyPair`
"""
warnings.warn('This method has been deprecated in favor of '
'create_key_pair method')
return self.create_key_pair(name=name)
def ex_import_keypair(self, name, keyfile):
"""
Import a KeyPair from a file
:param name: Name of the new KeyPair
:type name: ``str``
:param keyfile: Path to the public key file (in OpenSSH format)
:type keyfile: ``str``
:rtype: :class:`OpenStackKeyPair`
"""
warnings.warn('This method has been deprecated in favor of '
'import_key_pair_from_file method')
return self.import_key_pair_from_file(name=name, key_file_path=keyfile)
def ex_import_keypair_from_string(self, name, key_material):
"""
Import a KeyPair from a string
:param name: Name of the new KeyPair
:type name: ``str``
:param key_material: Public key (in OpenSSH format)
:type key_material: ``str``
:rtype: :class:`OpenStackKeyPair`
"""
warnings.warn('This method has been deprecated in favor of '
'import_key_pair_from_string method')
return self.import_key_pair_from_string(name=name,
key_material=key_material)
def ex_delete_keypair(self, keypair):
"""
Delete a KeyPair.
:param keypair: KeyPair to delete
:type keypair: :class:`OpenStackKeyPair`
:rtype: ``bool``
"""
warnings.warn('This method has been deprecated in favor of '
'delete_key_pair method')
return self.delete_key_pair(key_pair=keypair)
def ex_get_size(self, size_id):
"""
Get a NodeSize
:param size_id: ID of the size which should be used
:type size_id: ``str``
:rtype: :class:`NodeSize`
"""
return self._to_size(self.connection.request(
'/flavors/%s' % (size_id,)) .object['flavor'])
def get_image(self, image_id):
"""
Get a NodeImage
@inherits: :class:`NodeDriver.get_image`
:param image_id: ID of the image which should be used
:type image_id: ``str``
:rtype: :class:`NodeImage`
"""
return self._to_image(self.connection.request(
'/images/%s' % (image_id,)).object['image'])
def delete_image(self, image):
"""
Delete a NodeImage
@inherits: :class:`NodeDriver.delete_image`
:param image: image witch should be used
:type image: :class:`NodeImage`
:rtype: ``bool``
"""
resp = self.connection.request('/images/%s' % (image.id,),
method='DELETE')
return resp.status == httplib.NO_CONTENT
def _node_action(self, node, action, **params):
params = params or None
return self.connection.request('/servers/%s/action' % (node.id,),
method='POST', data={action: params})
def _update_node(self, node, **node_updates):
"""
Updates the editable attributes of a server, which currently include
its name and IPv4/IPv6 access addresses.
"""
return self._to_node(
self.connection.request(
'/servers/%s' % (node.id,), method='PUT',
data={'server': node_updates}
).object['server']
)
def _to_node_from_obj(self, obj):
return self._to_node(obj['server'])
def _to_node(self, api_node):
public_networks_labels = ['public', 'internet']
public_ips, private_ips = [], []
for label, values in api_node['addresses'].items():
for value in values:
ip = value['addr']
is_public_ip = False
try:
public_subnet = is_public_subnet(ip)
except:
# IPv6
public_subnet = False
# Openstack Icehouse sets 'OS-EXT-IPS:type' to 'floating' for
# public and 'fixed' for private
explicit_ip_type = value.get('OS-EXT-IPS:type', None)
if explicit_ip_type == 'floating':
is_public_ip = True
elif explicit_ip_type == 'fixed':
is_public_ip = False
elif label in public_networks_labels:
# Try label next
is_public_ip = True
elif public_subnet:
# Check for public subnet
is_public_ip = True
if is_public_ip:
public_ips.append(ip)
else:
private_ips.append(ip)
# Sometimes 'image' attribute is not present if the node is in an error
# state
image = api_node.get('image', None)
image_id = image.get('id', None) if image else None
config_drive = api_node.get("config_drive", False)
volumes_attached = api_node.get('os-extended-volumes:volumes_attached')
created = parse_date(api_node["created"])
return Node(
id=api_node['id'],
name=api_node['name'],
state=self.NODE_STATE_MAP.get(api_node['status'],
NodeState.UNKNOWN),
public_ips=public_ips,
private_ips=private_ips,
created_at=created,
driver=self,
extra=dict(
hostId=api_node['hostId'],
access_ip=api_node.get('accessIPv4'),
access_ipv6=api_node.get('accessIPv6', None),
# Docs says "tenantId", but actual is "tenant_id". *sigh*
# Best handle both.
tenantId=api_node.get('tenant_id') or api_node['tenantId'],
userId=api_node.get('user_id', None),
imageId=image_id,
flavorId=api_node['flavor']['id'],
uri=next(link['href'] for link in api_node['links'] if
link['rel'] == 'self'),
service_name=self.connection.get_service_name(),
metadata=api_node['metadata'],
password=api_node.get('adminPass', None),
created=api_node['created'],
updated=api_node['updated'],
key_name=api_node.get('key_name', None),
disk_config=api_node.get('OS-DCF:diskConfig', None),
config_drive=config_drive,
availability_zone=api_node.get('OS-EXT-AZ:availability_zone'),
volumes_attached=volumes_attached,
task_state=api_node.get("OS-EXT-STS:task_state", None),
vm_state=api_node.get("OS-EXT-STS:vm_state", None),
power_state=api_node.get("OS-EXT-STS:power_state", None),
progress=api_node.get("progress", None),
fault=api_node.get('fault')
),
)
def _to_volume(self, api_node):
if 'volume' in api_node:
api_node = api_node['volume']
state = self.VOLUME_STATE_MAP.get(api_node['status'],
StorageVolumeState.UNKNOWN)
return StorageVolume(
id=api_node['id'],
name=api_node['displayName'],
size=api_node['size'],
state=state,
driver=self,
extra={
'description': api_node['displayDescription'],
'attachments': [att for att in api_node['attachments'] if att],
# TODO: remove in 1.18.0
'state': api_node.get('status', None),
'snapshot_id': api_node.get('snapshotId', None),
'location': api_node.get('availabilityZone', None),
'volume_type': api_node.get('volumeType', None),
'metadata': api_node.get('metadata', None),
'created_at': api_node.get('createdAt', None)
}
)
def _to_snapshot(self, data):
if 'snapshot' in data:
data = data['snapshot']
volume_id = data.get('volume_id', data.get('volumeId', None))
display_name = data.get('display_name', data.get('displayName', None))
created_at = data.get('created_at', data.get('createdAt', None))
description = data.get('display_description',
data.get('displayDescription', None))
status = data.get('status', None)
extra = {'volume_id': volume_id,
'name': display_name,
'created': created_at,
'description': description,
'status': status}
state = self.SNAPSHOT_STATE_MAP.get(
status,
VolumeSnapshotState.UNKNOWN
)
try:
created_dt = parse_date(created_at)
except ValueError:
created_dt = None
snapshot = VolumeSnapshot(id=data['id'], driver=self,
size=data['size'], extra=extra,
created=created_dt, state=state)
return snapshot
def _to_size(self, api_flavor, price=None, bandwidth=None):
# if provider-specific subclasses can get better values for
# price/bandwidth, then can pass them in when they super().
if not price:
price = self._get_size_price(str(api_flavor['id']))
extra = api_flavor.get('OS-FLV-WITH-EXT-SPECS:extra_specs', {})
return OpenStackNodeSize(
id=api_flavor['id'],
name=api_flavor['name'],
ram=api_flavor['ram'],
disk=api_flavor['disk'],
vcpus=api_flavor['vcpus'],
ephemeral_disk=api_flavor.get('OS-FLV-EXT-DATA:ephemeral', None),
swap=api_flavor['swap'],
extra=extra,
bandwidth=bandwidth,
price=price,
driver=self,
)
def _get_size_price(self, size_id):
try:
return get_size_price(
driver_type='compute',
driver_name=self.api_name,
size_id=size_id,
)
except KeyError:
return(0.0)
def _extract_image_id_from_url(self, location_header):
path = urlparse.urlparse(location_header).path
image_id = path.split('/')[-1]
return image_id
def ex_rescue(self, node, password=None):
# Requires Rescue Mode extension
"""
Rescue a node
:param node: node
:type node: :class:`Node`
:param password: password
:type password: ``str``
:rtype: :class:`Node`
"""
if password:
resp = self._node_action(node, 'rescue', adminPass=password)
else:
resp = self._node_action(node, 'rescue')
password = json.loads(resp.body)['adminPass']
node.extra['password'] = password
return node
def ex_unrescue(self, node):
"""
Unrescue a node
:param node: node
:type node: :class:`Node`
:rtype: ``bool``
"""
resp = self._node_action(node, 'unrescue')
return resp.status == httplib.ACCEPTED
def _to_floating_ip_pools(self, obj):
pool_elements = obj['floating_ip_pools']
return [self._to_floating_ip_pool(pool) for pool in pool_elements]
def _to_floating_ip_pool(self, obj):
return OpenStack_1_1_FloatingIpPool(obj['name'], self.connection)
def ex_list_floating_ip_pools(self):
"""
List available floating IP pools
:rtype: ``list`` of :class:`OpenStack_1_1_FloatingIpPool`
"""
return self._to_floating_ip_pools(
self.connection.request('/os-floating-ip-pools').object)
def _to_floating_ips(self, obj):
ip_elements = obj['floating_ips']
return [self._to_floating_ip(ip) for ip in ip_elements]
def _to_floating_ip(self, obj):
return OpenStack_1_1_FloatingIpAddress(id=obj['id'],
ip_address=obj['ip'],
pool=None,
node_id=obj['instance_id'],
driver=self)
def ex_list_floating_ips(self):
"""
List floating IPs
:rtype: ``list`` of :class:`OpenStack_1_1_FloatingIpAddress`
"""
return self._to_floating_ips(
self.connection.request('/os-floating-ips').object)
def ex_get_floating_ip(self, ip):
"""
Get specified floating IP
:param ip: floating IP to get
:type ip: ``str``
:rtype: :class:`OpenStack_1_1_FloatingIpAddress`
"""
floating_ips = self.ex_list_floating_ips()
ip_obj, = [x for x in floating_ips if x.ip_address == ip]
return ip_obj
def ex_create_floating_ip(self):
"""
Create new floating IP
:rtype: :class:`OpenStack_1_1_FloatingIpAddress`
"""
resp = self.connection.request('/os-floating-ips',
method='POST',
data={})
data = resp.object['floating_ip']
id = data['id']
ip_address = data['ip']
return OpenStack_1_1_FloatingIpAddress(id=id,
ip_address=ip_address,
pool=None,
node_id=None,
driver=self)
def ex_delete_floating_ip(self, ip):
"""
Delete specified floating IP
:param ip: floating IP to remove
:type ip: :class:`OpenStack_1_1_FloatingIpAddress`
:rtype: ``bool``
"""
resp = self.connection.request('/os-floating-ips/%s' % ip.id,
method='DELETE')
return resp.status in (httplib.NO_CONTENT, httplib.ACCEPTED)
def ex_attach_floating_ip_to_node(self, node, ip):
"""
Attach the floating IP to the node
:param node: node
:type node: :class:`Node`
:param ip: floating IP to attach
:type ip: ``str`` or :class:`OpenStack_1_1_FloatingIpAddress`
:rtype: ``bool``
"""
address = ip.ip_address if hasattr(ip, 'ip_address') else ip
data = {
'addFloatingIp': {'address': address}
}
resp = self.connection.request('/servers/%s/action' % node.id,
method='POST', data=data)
return resp.status == httplib.ACCEPTED
def ex_detach_floating_ip_from_node(self, node, ip):
"""
Detach the floating IP from the node
:param node: node
:type node: :class:`Node`
:param ip: floating IP to remove
:type ip: ``str`` or :class:`OpenStack_1_1_FloatingIpAddress`
:rtype: ``bool``
"""
address = ip.ip_address if hasattr(ip, 'ip_address') else ip
data = {
'removeFloatingIp': {'address': address}
}
resp = self.connection.request('/servers/%s/action' % node.id,
method='POST', data=data)
return resp.status == httplib.ACCEPTED
def ex_get_metadata_for_node(self, node):
"""
Return the metadata associated with the node.
:param node: Node instance
:type node: :class:`Node`
:return: A dictionary or other mapping of strings to strings,
associating tag names with tag values.
:type tags: ``dict``
"""
return node.extra['metadata']
def ex_pause_node(self, node):
uri = '/servers/%s/action' % (node.id)
data = {'pause': None}
resp = self.connection.request(uri, method='POST', data=data)
return resp.status == httplib.ACCEPTED
def ex_unpause_node(self, node):
uri = '/servers/%s/action' % (node.id)
data = {'unpause': None}
resp = self.connection.request(uri, method='POST', data=data)
return resp.status == httplib.ACCEPTED
def ex_suspend_node(self, node):
uri = '/servers/%s/action' % (node.id)
data = {'suspend': None}
resp = self.connection.request(uri, method='POST', data=data)
return resp.status == httplib.ACCEPTED
def ex_resume_node(self, node):
uri = '/servers/%s/action' % (node.id)
data = {'resume': None}
resp = self.connection.request(uri, method='POST', data=data)
return resp.status == httplib.ACCEPTED
class OpenStack_1_1_FloatingIpPool(object):
"""
Floating IP Pool info.
"""
def __init__(self, name, connection):
self.name = name
self.connection = connection
def list_floating_ips(self):
"""
List floating IPs in the pool
:rtype: ``list`` of :class:`OpenStack_1_1_FloatingIpAddress`
"""
return self._to_floating_ips(
self.connection.request('/os-floating-ips').object)
def _to_floating_ips(self, obj):
ip_elements = obj['floating_ips']
return [self._to_floating_ip(ip) for ip in ip_elements]
def _to_floating_ip(self, obj):
return OpenStack_1_1_FloatingIpAddress(id=obj['id'],
ip_address=obj['ip'],
pool=self,
node_id=obj['instance_id'],
driver=self.connection.driver)
def get_floating_ip(self, ip):
"""
Get specified floating IP from the pool
:param ip: floating IP to get
:type ip: ``str``
:rtype: :class:`OpenStack_1_1_FloatingIpAddress`
"""
ip_obj, = [x for x in self.list_floating_ips() if x.ip_address == ip]
return ip_obj
def create_floating_ip(self):
"""
Create new floating IP in the pool
:rtype: :class:`OpenStack_1_1_FloatingIpAddress`
"""
resp = self.connection.request('/os-floating-ips',
method='POST',
data={'pool': self.name})
data = resp.object['floating_ip']
id = data['id']
ip_address = data['ip']
return OpenStack_1_1_FloatingIpAddress(id=id,
ip_address=ip_address,
pool=self,
node_id=None,
driver=self.connection.driver)
def delete_floating_ip(self, ip):
"""
Delete specified floating IP from the pool
:param ip: floating IP to remove
:type ip::class:`OpenStack_1_1_FloatingIpAddress`
:rtype: ``bool``
"""
resp = self.connection.request('/os-floating-ips/%s' % ip.id,
method='DELETE')
return resp.status in (httplib.NO_CONTENT, httplib.ACCEPTED)
def __repr__(self):
return ('<OpenStack_1_1_FloatingIpPool: name=%s>' % self.name)
class OpenStack_1_1_FloatingIpAddress(object):
"""
Floating IP info.
"""
def __init__(self, id, ip_address, pool, node_id=None, driver=None):
self.id = str(id)
self.ip_address = ip_address
self.pool = pool
self.node_id = node_id
self.driver = driver
def delete(self):
"""
Delete this floating IP
:rtype: ``bool``
"""
if self.pool is not None:
return self.pool.delete_floating_ip(self)
elif self.driver is not None:
return self.driver.ex_delete_floating_ip(self)
def __repr__(self):
return ('<OpenStack_1_1_FloatingIpAddress: id=%s, ip_addr=%s,'
' pool=%s, driver=%s>'
% (self.id, self.ip_address, self.pool, self.driver))