# 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.
"""
Driver for IBM SmartCloud Enterprise
Formerly known as:
- IBM Developer Cloud
- IBM Smart Business Development and Test on the IBM Cloud
- IBM SmartBusiness Cloud
"""
import base64
import time
from libcloud.utils.py3 import urlencode
from libcloud.utils.py3 import httplib
from libcloud.utils.py3 import b
from libcloud.common.base import XmlResponse, ConnectionUserAndKey
from libcloud.common.types import InvalidCredsError, LibcloudError
from libcloud.compute.types import NodeState, Provider
from libcloud.compute.base import NodeDriver, Node, NodeImage, \
NodeSize, NodeLocation, NodeAuthSSHKey, StorageVolume
HOST = 'www-147.ibm.com'
REST_BASE = '/computecloud/enterprise/api/rest/20100331'
[docs]class IBMResponse(XmlResponse):
[docs] def success(self):
return int(self.status) == 200
[docs] def parse_error(self):
if int(self.status) == 401:
if not self.body:
raise InvalidCredsError(str(self.status) + ': ' + self.error)
else:
raise InvalidCredsError(self.body)
return self.body
[docs]class IBMConnection(ConnectionUserAndKey):
"""
Connection class for the IBM SmartCloud Enterprise driver
"""
host = HOST
responseCls = IBMResponse
[docs] def encode_data(self, data):
return urlencode(data)
[docs]class IBMNodeLocation(NodeLocation):
"""
Extends the base LibCloud NodeLocation to contain additional attributes
"""
def __init__(self, id, name, country, driver, extra=None):
self.id = str(id)
self.name = name
self.country = country
self.driver = driver
self.extra = extra or {}
def __repr__(self):
return ('<IBMNodeLocation: id=%s, name=%s, country=%s, '
'driver=%s, extra=%s>' %
(self.id, self.name, self.country, self.driver.name,
self.extra))
[docs]class VolumeState(object):
"""
The SCE specific states for a storage volume
"""
NEW = '0'
CREATING = '1'
DELETING = '2'
DELETED = '3'
DETACHED = '4'
ATTACHED = '5'
FAILED = '6'
DELETE_PENDING = '7'
BEING_CLONED = '8'
CLONING = '9'
ATTACHING = '10'
DETACHING = '11'
ATTACHIED = '12'
IMPORTING = '13'
TRANSFER_RETRYING = '14'
[docs]class VolumeOffering(object):
"""
An SCE specific storage volume offering class.
The volume offering ID is needed to create a volume.
Volume offering IDs are different for each data center.
"""
def __init__(self, id, name, location, extra=None):
self.id = id
self.location = location
self.name = name
self.extra = extra or {}
def __repr__(self):
return ('<VolumeOffering: id=%s, location=%s, name=%s, extra=%s>' %
(self.id, self.location, self.name, self.extra))
[docs]class Address(object):
"""
A reserved IP address that can be attached to an instance.
Properties: id, ip, state, options(location, type, created_time, state,
hostname, instance_ids, vlan, owner,
mode, offering_id)
"""
def __init__(self, id, ip, state, options):
self.id = id
self.ip = ip
self.state = state
self.options = options
def __repr__(self):
return ('<Address: id=%s, ip=%s, state=%s, options=%s>' %
(self.id, self.ip, self.state, self.options))
[docs]class IBMNodeDriver(NodeDriver):
"""
Node driver for IBM SmartCloud Enterprise
"""
connectionCls = IBMConnection
type = Provider.IBM
name = "IBM SmartCloud Enterprise"
website = 'http://ibm.com/services/us/en/cloud-enterprise/'
NODE_STATE_MAP = {
0: NodeState.PENDING, # New
1: NodeState.PENDING, # Provisioning
2: NodeState.TERMINATED, # Failed
3: NodeState.TERMINATED, # Removed
4: NodeState.TERMINATED, # Rejected
5: NodeState.RUNNING, # Active
6: NodeState.UNKNOWN, # Unknown
7: NodeState.PENDING, # Deprovisioning
8: NodeState.REBOOTING, # Restarting
9: NodeState.PENDING, # Starting
10: NodeState.PENDING, # Stopping
11: NodeState.TERMINATED, # Stopped
12: NodeState.PENDING, # Deprovision Pending
13: NodeState.PENDING, # Restart Pending
14: NodeState.PENDING, # Attaching
15: NodeState.PENDING, # Detaching
}
[docs] def create_node(self, **kwargs):
"""
Creates a node in the IBM SmartCloud Enterprise.
See :class:`NodeDriver.create_node` for more keyword args.
@inherits: :class:`NodeDriver.create_node`
:keyword auth: Name of the pubkey to use. When constructing
:class:`NodeAuthSSHKey` instance, 'pubkey' argument must be the
name of the public key to use. You chose this name when creating
a new public key on the IBM server.
:type auth: :class:`NodeAuthSSHKey`
:keyword ex_configurationData: Image-specific configuration
parameters. Configuration parameters are defined in the parameters
.xml file. The URL to this file is defined in the NodeImage at
extra[parametersURL].
Note: This argument must be specified when launching a Windows
instance. It must contain 'UserName' and 'Password' keys.
:type ex_configurationData: ``dict``
"""
# Compose headers for message body
data = {}
data.update({'name': kwargs['name']})
data.update({'imageID': kwargs['image'].id})
data.update({'instanceType': kwargs['size'].id})
if 'location' in kwargs:
data.update({'location': kwargs['location'].id})
else:
data.update({'location': '1'})
if 'auth' in kwargs and isinstance(kwargs['auth'], NodeAuthSSHKey):
data.update({'publicKey': kwargs['auth'].pubkey})
if 'ex_configurationData' in kwargs:
configurationData = kwargs['ex_configurationData']
if configurationData:
for key in configurationData.keys():
data.update({key: configurationData.get(key)})
# Send request!
resp = self.connection.request(
action=REST_BASE + '/instances',
headers={'Content-Type': 'application/x-www-form-urlencoded'},
method='POST',
data=data).object
return self._to_nodes(resp)[0]
[docs] def create_volume(self, size, name, location, **kwargs):
"""
Create a new block storage volume (virtual disk)
:param size: Size of volume in gigabytes (required).
Find out the possible sizes from the
offerings/storage REST interface
:type size: ``int``
:keyword name: Name of the volume to be created (required)
:type name: ``str``
:keyword location: Which data center to create a volume in. If
empty, it will fail for IBM SmartCloud Enterprise
(required)
:type location: :class:`NodeLocation`
:keyword snapshot: Not supported for IBM SmartCloud Enterprise
:type snapshot: ``str``
:keyword kwargs.format: Either RAW or EXT3 for IBM SmartCloud
Enterprise (optional)
:type kwargs.format: ``str``
:keyword kwargs.offering_id: The storage offering ID for IBM
SmartCloud Enterprise
Find this from the REST interface
storage/offerings. (optional)
:type kwargs.offering_id: ``str``
:keyword kwargs.source_disk_id: If cloning a volume, the storage
disk to make a copy from (optional)
:type kwargs.source_disk_id: ``str``
:keyword kwargs.storage_area_id: The id of the storage availability
area to create the volume in
(optional)
:type kwargs.storage_area_id: ``str``
:keyword kwargs.target_location_id: If cloning a volume, the
storage disk to make a copy
from (optional)
:type kwargs.target_location_id: ``str``
:return: The newly created :class:`StorageVolume`.
:rtype: :class:`StorageVolume`
"""
data = {}
data.update({'name': name})
data.update({'size': size})
data.update({'location': location})
if (('format' in kwargs) and (kwargs['format'] is not None)):
data.update({'format': kwargs['format']})
if (('offering_id' in kwargs) and (kwargs['offering_id'] is not None)):
data.update({'offeringID': kwargs['offering_id']})
if (('storage_area_id' in kwargs) and
(kwargs['storage_area_id'] is not None)):
data.update({'storageAreaID': kwargs['storage_area_id']})
if 'source_disk_id' in kwargs:
data.update({'sourceDiskID': kwargs['source_disk_id']})
data.update({'type': 'clone'})
if 'target_location_id' in kwargs:
data.update({'targetLocationID': kwargs['target_location_id']})
resp = self.connection.request(
action=REST_BASE + '/storage',
headers={'Content-Type': 'application/x-www-form-urlencoded'},
method='POST',
data=data).object
return self._to_volumes(resp)[0]
[docs] def create_image(self, name, description=None, **kwargs):
"""
Create a new node image from an existing volume or image.
:param name: Name of the image to be created (required)
:type name: ``str``
:param description: Description of the image to be created
:type description: ``str``
:keyword image_id: The ID of the source image if cloning the image
:type image_id: ``str``
:keyword volume_id: The ID of the storage volume if
importing the image
:type volume_id: ``str``
:return: The newly created :class:`NodeImage`.
:rtype: :class:`NodeImage`
"""
data = {}
data.update({'name': name})
if description is not None:
data.update({'description': description})
if (('image_id' in kwargs) and (kwargs['image_id'] is not None)):
data.update({'imageId': kwargs['image_id']})
if (('volume_id' in kwargs) and (kwargs['volume_id'] is not None)):
data.update({'volumeId': kwargs['volume_id']})
resp = self.connection.request(
action=REST_BASE + '/offerings/image',
headers={'Content-Type': 'application/x-www-form-urlencoded'},
method='POST',
data=data).object
return self._to_images(resp)[0]
[docs] def destroy_node(self, node):
url = REST_BASE + '/instances/%s' % (node.id)
status = int(self.connection.request(action=url,
method='DELETE').status)
return status == httplib.OK
[docs] def destroy_volume(self, volume):
"""
Destroys a storage volume.
:param volume: Volume to be destroyed
:type volume: :class:`StorageVolume`
:rtype: ``bool``
"""
url = REST_BASE + '/storage/%s' % (volume.id)
status = int(self.connection.request(action=url,
method='DELETE').status)
return status == httplib.OK
[docs] def ex_destroy_image(self, image):
"""
Destroys an image.
:param image: Image to be destroyed
:type image: :class:`NodeImage`
:return: ``bool``
"""
url = REST_BASE + '/offerings/image/%s' % (image.id)
status = int(self.connection.request(action=url,
method='DELETE').status)
return status == 200
[docs] def attach_volume(self, node, volume):
"""
Attaches volume to node.
:param node: Node to attach volume to
:type node: :class:`Node`
:param volume: Volume to attach
:type volume: :class:`StorageVolume`
:rtype: ``bool``
"""
url = REST_BASE + '/instances/%s' % (node.id)
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
data = {'storageID': volume.id, 'type': 'attach'}
resp = self.connection.request(action=url,
method='PUT',
headers=headers,
data=data)
return int(resp.status) == 200
[docs] def detach_volume(self, node, volume):
"""
Detaches a volume from a node.
:param node: Node which should be used
:type node: :class:`Node`
:param volume: Volume to be detached
:type volume: :class:`StorageVolume`
:rtype: ``bool``
"""
url = REST_BASE + '/instances/%s' % (node.id)
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
data = {'storageID': volume.id, 'type': 'detach'}
resp = self.connection.request(action=url,
method='PUT',
headers=headers,
data=data)
return int(resp.status) == 200
[docs] def reboot_node(self, node):
url = REST_BASE + '/instances/%s' % (node.id)
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
data = {'state': 'restart'}
resp = self.connection.request(action=url,
method='PUT',
headers=headers,
data=data)
return int(resp.status) == 200
[docs] def list_nodes(self):
return self._to_nodes(
self.connection.request(REST_BASE + '/instances').object)
[docs] def list_images(self, location=None):
return self._to_images(
self.connection.request(REST_BASE + '/offerings/image').object)
[docs] def list_volumes(self):
"""
List storage volumes.
:rtype: ``list`` of :class:`StorageVolume`
"""
return self._to_volumes(
self.connection.request(REST_BASE + '/storage').object)
[docs] def list_sizes(self, location=None):
"""
Returns a generic list of sizes. See list_images() for a list of
supported sizes for specific images. In particular, you need to have
a size that matches the architecture (32-bit vs 64-bit) of the virtual
machine image operating system.
@inherits: :class:`NodeDriver.list_sizes`
"""
return [
NodeSize('BRZ32.1/2048/60*175', 'Bronze 32 bit', None, None, None,
None, self.connection.driver),
NodeSize('BRZ64.2/4096/60*500*350', 'Bronze 64 bit', None, None,
None, None, self.connection.driver),
NodeSize('COP32.1/2048/60', 'Copper 32 bit', None, None, None,
None, self.connection.driver),
NodeSize('COP64.2/4096/60', 'Copper 64 bit', None, None, None,
None, self.connection.driver),
NodeSize('SLV32.2/4096/60*350', 'Silver 32 bit', None, None, None,
None, self.connection.driver),
NodeSize('SLV64.4/8192/60*500*500', 'Silver 64 bit', None, None,
None, None, self.connection.driver),
NodeSize('GLD32.4/4096/60*350', 'Gold 32 bit', None, None, None,
None, self.connection.driver),
NodeSize('GLD64.8/16384/60*500*500', 'Gold 64 bit', None, None,
None, None, self.connection.driver),
NodeSize('PLT64.16/16384/60*500*500*500*500', 'Platinum 64 bit',
None, None, None, None, self.connection.driver)]
[docs] def list_locations(self):
return self._to_locations(
self.connection.request(REST_BASE + '/locations').object)
[docs] def ex_list_storage_offerings(self):
"""
List the storage center offerings
:rtype: ``list`` of :class:`VolumeOffering`
"""
return self._to_volume_offerings(
self.connection.request(REST_BASE + '/offerings/storage').object)
[docs] def ex_allocate_address(self, location_id, offering_id, vlan_id=None):
"""
Allocate a new reserved IP address
:param location_id: Target data center
:type location_id: ``str``
:param offering_id: Offering ID for address to create
:type offering_id: ``str``
:param vlan_id: ID of target VLAN
:type vlan_id: ``str``
:return: :class:`Address` object
:rtype: :class:`Address`
"""
url = REST_BASE + '/addresses'
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
data = {'location': location_id, 'offeringID': offering_id}
if vlan_id is not None:
data.update({'vlanID': vlan_id})
resp = self.connection.request(action=url,
method='POST',
headers=headers,
data=data).object
return self._to_addresses(resp)[0]
[docs] def ex_list_addresses(self, resource_id=None):
"""
List the reserved IP addresses
:param resource_id: If this is supplied only a single address will
be returned (optional)
:type resource_id: ``str``
:rtype: ``list`` of :class:`Address`
"""
url = REST_BASE + '/addresses'
if resource_id:
url += '/' + resource_id
return self._to_addresses(self.connection.request(url).object)
[docs] def ex_copy_to(self, image, volume):
"""
Copies a node image to a storage volume
:param image: source image to copy
:type image: :class:`NodeImage`
:param volume: Target storage volume to copy to
:type volume: :class:`StorageVolume`
:return: ``bool`` The success of the operation
:rtype: ``bool``
"""
url = REST_BASE + '/storage/%s' % (volume.id)
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
data = {'imageId': image.id}
resp = self.connection.request(action=url,
method='PUT',
headers=headers,
data=data)
return int(resp.status) == 200
[docs] def ex_delete_address(self, resource_id):
"""
Delete a reserved IP address
:param resource_id: The address to delete (required)
:type resource_id: ``str``
:rtype: ``bool``
"""
url = REST_BASE + '/addresses/' + resource_id
status = int(self.connection.request(action=url,
method='DELETE').status)
return status == 200
[docs] def ex_wait_storage_state(self, volume, state=VolumeState.DETACHED,
wait_period=60, timeout=1200):
"""
Block until storage volume state changes to the given value
:param volume: Storage volume.
:type volume: :class:`StorageVolume`
:param state: The target state to wait for
:type state: ``int``
:param wait_period: How many seconds to between each loop
iteration (default is 3)
:type wait_period: ``int``
:param timeout: How many seconds to wait before timing out
(default is 1200)
:type timeout: ``int``
:rtype: :class:`StorageVolume`
"""
start = time.time()
end = start + timeout
while time.time() < end:
volumes = self.list_volumes()
volumes = list([v for v in volumes if v.uuid == volume.uuid])
if (len(volumes) == 1 and volumes[0].extra['state'] == state):
return volumes[0]
else:
time.sleep(wait_period)
continue
raise LibcloudError(value='Timed out after %d seconds' % (timeout),
driver=self)
def _to_nodes(self, object):
return [self._to_node(instance) for instance in
object.findall('Instance')]
def _to_node(self, instance):
public_ips = []
ip = instance.findtext('IP')
if ip:
public_ips.append(ip)
return Node(
id=instance.findtext('ID'),
name=instance.findtext('Name'),
state=self.NODE_STATE_MAP[int(instance.findtext('Status'))],
public_ips=public_ips,
private_ips=[],
driver=self.connection.driver
)
def _to_images(self, object):
# Converts data retrieved from SCE /offerings/image REST call to
# a NodeImage
return [self._to_image(image) for image in object.findall('Image')]
def _to_image(self, image):
# Converts an SCE Image object to a NodeImage
imageID = image.findtext('ID')
imageName = image.findtext('Name')
parametersURL = image.findtext('Manifest')
location = image.findtext('Location')
state = image.findtext('State')
owner = image.findtext('Owner')
visibility = image.findtext('Visibility')
platform = image.findtext('Platform')
description = image.findtext('Description')
documentation = image.findtext('Documentation')
instanceTypes = image.findall('SupportedInstanceTypes')
nodeSizes = self._to_node_sizes(image.find('SupportedInstanceTypes'))
return NodeImage(id=imageID,
name=imageName,
driver=self.connection.driver,
extra={
'parametersURL': parametersURL,
'location': location,
'state': state,
'owner': owner,
'visibility': visibility,
'platform': platform,
'description': description,
'documentation': documentation,
'instanceTypes': instanceTypes,
'node_sizes': nodeSizes
}
)
def _to_locations(self, object):
return [self._to_location(location) for location in
object.findall('Location')]
def _to_location(self, location):
# Converts an SCE Location object to a Libcloud NodeLocation object
name_text = location.findtext('Name')
description = location.findtext('Description')
state = location.findtext('State')
(nameVal, separator, countryVal) = name_text.partition(',')
capabiltyElements = location.findall('Capabilities/Capability')
capabilities = {}
for elem in capabiltyElements:
capabilityID = elem.attrib['id']
entryElements = elem.findall('Entry')
entries = []
for entryElem in entryElements:
key = entryElem.attrib['key']
valueElements = elem.findall('Value')
values = []
for valueElem in valueElements:
values.append(valueElem.text)
entry = {'key': key, 'values': values}
entries.append(entry)
capabilities[capabilityID] = entries
extra = {'description': description, 'state': state,
'capabilities': capabilities}
return IBMNodeLocation(id=location.findtext('ID'),
name=nameVal,
country=countryVal.strip(),
driver=self.connection.driver,
extra=extra)
def _to_node_sizes(self, object):
# Converts SCE SupportedInstanceTypes object to
# a list of Libcloud NodeSize objects
return [self._to_node_size(iType) for iType in
object.findall('InstanceType')]
def _to_node_size(self, object):
# Converts to an SCE InstanceType to a Libcloud NodeSize
return NodeSize(object.findtext('ID'),
object.findtext('Label'),
None,
None,
None,
object.findtext('Price/Rate'),
self.connection.driver)
def _to_volumes(self, object):
return [self._to_volume(iType) for iType in
object.findall('Volume')]
def _to_volume(self, object):
# Converts an SCE Volume to a Libcloud StorageVolume
extra = {'state': object.findtext('State'),
'location': object.findtext('Location'),
'instanceID': object.findtext('instanceID'),
'owner': object.findtext('Owner'),
'format': object.findtext('Format'),
'createdTime': object.findtext('CreatedTime'),
'storageAreaID': object.findtext('StorageArea/ID')}
return StorageVolume(object.findtext('ID'),
object.findtext('Name'),
object.findtext('Size'),
self.connection.driver,
extra)
def _to_volume_offerings(self, object):
return [self._to_volume_offering(iType) for iType in
object.findall('Offerings')]
def _to_volume_offering(self, object):
# Converts an SCE DescribeVolumeOfferingsResponse/Offerings XML object
# to an SCE VolumeOffering
extra = {'label': object.findtext('Label'),
'supported_sizes': object.findtext('SupportedSizes'),
'formats': object.findall('SupportedFormats/Format/ID'),
'price': object.findall('Price')}
return VolumeOffering(object.findtext('ID'),
object.findtext('Name'),
object.findtext('Location'),
extra)
def _to_addresses(self, object):
# Converts an SCE DescribeAddressesResponse XML object to a list of
# Address objects
return [self._to_address(iType) for iType in
object.findall('Address')]
def _to_address(self, object):
# Converts an SCE DescribeAddressesResponse/Address XML object to
# an Address object
extra = {'location': object.findtext('Location'),
'type': object.findtext('Label'),
'created_time': object.findtext('SupportedSizes'),
'hostname': object.findtext('Hostname'),
'instance_ids': object.findtext('InstanceID'),
'vlan': object.findtext('VLAN'),
'owner': object.findtext('owner'),
'mode': object.findtext('Mode'),
'offering_id': object.findtext('OfferingID')}
return Address(object.findtext('ID'),
object.findtext('IP'),
object.findtext('State'),
extra)