Source code for libcloud.compute.drivers.ibm_sce

# 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'


class IBMResponse(XmlResponse):
    def success(self):
        return int(self.status) == 200

    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


class IBMConnection(ConnectionUserAndKey):
    """
    Connection class for the IBM SmartCloud Enterprise driver
    """

    host = HOST
    responseCls = IBMResponse

    def add_default_headers(self, headers):
        headers['Accept'] = 'text/xml'
        headers['Authorization'] = ('Basic %s' % (base64.b64encode(
            b('%s:%s' % (self.user_id, self.key))).decode('utf-8')))
        if not 'Content-Type' in headers:
            headers['Content-Type'] = 'text/xml'
        return headers

    def encode_data(self, data):
        return urlencode(data)


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))


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'


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))


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]
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
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 def list_nodes(self): return self._to_nodes( self.connection.request(REST_BASE + '/instances').object) 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)]
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)