You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by an...@apache.org on 2016/11/14 04:18:05 UTC
[5/9] libcloud git commit: [LIBCLOUD-873] Updated ProfitBricks
Compute Driver (REST api v3)
http://git-wip-us.apache.org/repos/asf/libcloud/blob/2569a5f2/libcloud/compute/drivers/profitbricks.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/profitbricks.py b/libcloud/compute/drivers/profitbricks.py
index ab217f0..8013e3b 100644
--- a/libcloud/compute/drivers/profitbricks.py
+++ b/libcloud/compute/drivers/profitbricks.py
@@ -16,23 +16,22 @@
"""
import base64
+import json
import copy
import time
+import urllib
-try:
- from lxml import etree as ET
-except ImportError:
- from xml.etree import ElementTree as ET
-
-from libcloud.utils.networking import is_private_subnet
from libcloud.utils.py3 import b
from libcloud.compute.providers import Provider
-from libcloud.common.base import ConnectionUserAndKey, XmlResponse
+from libcloud.common.base import ConnectionUserAndKey, JsonResponse
from libcloud.compute.base import Node, NodeDriver, NodeLocation, NodeSize
-from libcloud.compute.base import NodeImage, StorageVolume
+from libcloud.compute.base import NodeImage, StorageVolume, VolumeSnapshot
from libcloud.compute.base import UuidMixin
from libcloud.compute.types import NodeState
from libcloud.common.types import LibcloudError, MalformedResponseError
+from libcloud.common.exceptions import BaseHTTPError
+
+from collections import defaultdict
__all__ = [
'API_VERSION',
@@ -40,41 +39,52 @@ __all__ = [
'ProfitBricksNodeDriver',
'Datacenter',
'ProfitBricksNetworkInterface',
- 'ProfitBricksAvailabilityZone'
+ 'ProfitBricksFirewallRule',
+ 'ProfitBricksLan',
+ 'ProfitBricksLoadBalancer',
+ 'ProfitBricksAvailabilityZone',
+ 'ProfitBricksIPBlock'
]
API_HOST = 'api.profitbricks.com'
-API_VERSION = '/1.3/'
+API_VERSION = '/cloudapi/v3/'
-class ProfitBricksResponse(XmlResponse):
+class ProfitBricksResponse(JsonResponse):
"""
ProfitBricks response parsing.
"""
def parse_error(self):
+ http_code = None
+ fault_code = None
+ message = None
try:
- body = ET.XML(self.body)
- except:
- raise MalformedResponseError('Failed to parse XML',
- body=self.body,
- driver=ProfitBricksNodeDriver)
-
- for e in body.findall('.//detail'):
- if ET.iselement(e[0].find('httpCode')):
- http_code = e[0].find('httpCode').text
- else:
- http_code = None
- if ET.iselement(e[0].find('faultCode')):
- fault_code = e[0].find('faultCode').text
+ body = json.loads(self.body)
+ if 'httpStatus' in body:
+ http_code = body['httpStatus']
else:
- fault_code = None
- if ET.iselement(e[0].find('message')):
- message = e[0].find('message').text
+ http_code = 'unknown'
+
+ if 'messages' in body:
+ message = ', '.join(list(map(
+ lambda item: item['message'], body['messages'])))
+ fault_code = ', '.join(list(map(
+ lambda item: item['errorCode'], body['messages'])))
else:
- message = None
+ message = 'No messages returned.'
+ fault_code = 'unknown'
+ except Exception:
+ raise MalformedResponseError('Failed to parse Json',
+ body=self.body,
+ driver=ProfitBricksNodeDriver)
- return LibcloudError('HTTP Code: %s, Fault Code: %s, Message: %s' %
- (http_code, fault_code, message), driver=self)
+ return LibcloudError(
+ '''
+ HTTP Code: %s,
+ Fault Code(s): %s,
+ Message(s): %s
+ '''
+ % (http_code, fault_code, message), driver=self)
class ProfitBricksConnection(ConnectionUserAndKey):
@@ -85,56 +95,53 @@ class ProfitBricksConnection(ConnectionUserAndKey):
api_prefix = API_VERSION
responseCls = ProfitBricksResponse
- # Supporting xml + lxml is funky :S
- SOAPENV_NAMESPACE = 'http://schemas.xmlsoap.org/soap/envelope/'
- SOAPENV = '{%s}' % SOAPENV_NAMESPACE
- WS_NAMESPACE = 'http://ws.api.profitbricks.com/'
- WS = '{%s}' % WS_NAMESPACE
- NSMAP = {
- 'soapenv': SOAPENV_NAMESPACE,
- 'ws': WS_NAMESPACE,
- }
-
def add_default_headers(self, headers):
- headers['Content-Type'] = 'text/xml'
headers['Authorization'] = 'Basic %s' % (base64.b64encode(
b('%s:%s' % (self.user_id, self.key))).decode('utf-8'))
return headers
def encode_data(self, data):
- soap_env = ET.Element(self.SOAPENV + 'Envelope',
- self.NSMAP, **self.NSMAP)
- ET.SubElement(soap_env, self.SOAPENV + 'Header')
- soap_body = ET.SubElement(soap_env, self.SOAPENV + 'Body')
- soap_req_body = ET.SubElement(soap_body, self.WS + data['action'])
-
- if 'request' in data.keys():
- soap_req_body = ET.SubElement(soap_req_body, 'request')
- for key, value in data.items():
- if key not in ['action', 'request']:
- child = ET.SubElement(soap_req_body, key)
- child.text = value
- else:
- for key, value in data.items():
- if key != 'action':
- child = ET.SubElement(soap_req_body, key)
- child.text = value
+ '''
+ If a string is passed in, just return it
+ or else if a dict is passed in, encode it
+ as a json string.
+ '''
+ if type(data) is str:
+ return data
- soap_post = ET.tostring(soap_env)
+ elif type(data) is dict:
+ return json.dumps(data)
- return soap_post
+ else:
+ return ''
def request(self, action, params=None, data=None, headers=None,
- method='POST', raw=False):
- action = self.api_prefix + action
+ method='GET', raw=False, with_full_url=False):
+
+ '''
+ Some requests will use the href attribute directly.
+ If this is not the case, then we should formulate the
+ url based on the action specified.
+ If we are using a full url, we need to remove the
+ host and protocol components.
+ '''
+ if not with_full_url or with_full_url is False:
+ action = self.api_prefix + action
+ else:
+ action = action.replace(
+ 'https://{host}'.format(host=self.host),
+ ''
+ )
- return super(ProfitBricksConnection, self).request(action=action,
- params=params,
- data=data,
- headers=headers,
- method=method,
- raw=raw)
+ return super(ProfitBricksConnection, self).request(
+ action=action,
+ params=params,
+ data=data,
+ headers=headers,
+ method=method,
+ raw=raw
+ )
class Datacenter(UuidMixin):
@@ -145,17 +152,26 @@ class Datacenter(UuidMixin):
:param id: The datacenter ID.
:type id: ``str``
+ :param href: The datacenter href.
+ :type href: ``str``
+
:param name: The datacenter name.
:type name: ``str``
- :param version: Datacenter version.
- :type version: ``str``
+ :param version: Datacenter version.
+ :type version: ``str``
+
+ :param driver: ProfitBricks Node Driver.
+ :type driver: :class:`ProfitBricksNodeDriver`
+ :param extra: Extra properties for the Datacenter.
+ :type extra: ``dict``
Note: This class is ProfitBricks specific.
"""
- def __init__(self, id, name, version, driver, extra=None):
+ def __init__(self, id, href, name, version, driver, extra=None):
self.id = str(id)
+ self.href = href
self.name = name
self.version = version
self.driver = driver
@@ -164,8 +180,8 @@ class Datacenter(UuidMixin):
def __repr__(self):
return ((
- '<Datacenter: id=%s, name=%s, version=%s, driver=%s> ...>')
- % (self.id, self.name, self.version,
+ '<Datacenter: id=%s, href=%s name=%s, version=%s, driver=%s> ...>')
+ % (self.id, self.href, self.name, self.version,
self.driver.name))
@@ -180,20 +196,141 @@ class ProfitBricksNetworkInterface(object):
:param name: The network interface name.
:type name: ``str``
+ :param href: The network interface href.
+ :type href: ``str``
+
:param state: The network interface name.
:type state: ``int``
+ :param extra: Extra properties for the network interface.
+ :type extra: ``dict``
+
+ Note: This class is ProfitBricks specific.
+ """
+ def __init__(self, id, name, href, state, extra=None):
+ self.id = id
+ self.name = name
+ self.href = href
+ self.state = state
+ self.extra = extra or {}
+
+ def __repr__(self):
+ return (('<ProfitBricksNetworkInterface: id=%s, name=%s, href=%s>')
+ % (self.id, self.name, self.href))
+
+
+class ProfitBricksFirewallRule(object):
+ """
+ Extension class which stores information about a ProfitBricks
+ firewall rule.
+
+ :param id: The firewall rule ID.
+ :type id: ``str``
+
+ :param name: The firewall rule name.
+ :type name: ``str``
+
+ :param href: The firewall rule href.
+ :type href: ``str``
+
+ :param state: The current state of the firewall rule.
+ :type state: ``int``
+
+ :param extra: Extra properties for the firewall rule.
+ :type extra: ``dict``
+
+ Note: This class is ProfitBricks specific.
+
+ """
+
+ def __init__(self, id, name, href, state, extra=None):
+ self.id = id
+ self.name = name
+ self.href = href
+ self.state = state
+ self.extra = extra or {}
+
+ def __repr__(self):
+ return (('<ProfitBricksFirewallRule: id=%s, name=%s, href=%s>')
+ % (self.id, self.name, self.href))
+
+
+class ProfitBricksLan(object):
+ """
+ Extension class which stores information about a
+ ProfitBricks LAN
+
+ :param id: The ID of the lan.
+ :param id: ``str``
+
+ :param name: The name of the lan.
+ :type name: ``str``
+
+ :param href: The lan href.
+ :type href: ``str``
+
+ :param is_public: If public, the lan faces the public internet.
+ :type is_public: ``bool``
+
+ :param state: The current state of the lan.
+ :type state: ``int``
+
+ :param extra: Extra properties for the lan.
+ :type extra: ``dict``
+
Note: This class is ProfitBricks specific.
+
+ """
+
+ def __init__(self, id, name, href, is_public, state, driver, extra=None):
+ self.id = id
+ self.name = name
+ self.href = href
+ self.is_public = is_public
+ self.state = state
+ self.driver = driver
+ self.extra = extra or {}
+
+ def __repr__(self):
+ return (('<ProfitBricksLan: id=%s, name=%s, href=%s>')
+ % (self.id, self.name, self.href))
+
+
+class ProfitBricksLoadBalancer(object):
+ """
+ Extention class which stores information about a
+ ProfitBricks load balancer
+
+ :param id: The ID of the load balancer.
+ :param id: ``str``
+
+ :param name: The name of the load balancer.
+ :type name: ``str``
+
+ :param href: The load balancer href.
+ :type href: ``str``
+
+ :param state: The current state of the load balancer.
+ :type state: ``int``
+
+ :param extra: Extra properties for the load balancer.
+ :type extra: ``dict``
+
+ Note: This class is ProfitBricks specific
+
"""
- def __init__(self, id, name, state, extra=None):
+
+ def __init__(self, id, name, href, state, driver, extra=None):
self.id = id
self.name = name
+ self.href = href
self.state = state
+ self.driver = driver
self.extra = extra or {}
def __repr__(self):
- return (('<ProfitBricksNetworkInterface: id=%s, name=%s>')
- % (self.id, self.name))
+ return (('ProfitBricksLoadbalancer: id=%s, name=%s, href=%s>')
+ % (self.id, self.name, self.href))
class ProfitBricksAvailabilityZone(object):
@@ -201,6 +338,9 @@ class ProfitBricksAvailabilityZone(object):
Extension class which stores information about a ProfitBricks
availability zone.
+ :param name: The availability zone name.
+ :type name: ``str``
+
Note: This class is ProfitBricks specific.
"""
@@ -212,6 +352,64 @@ class ProfitBricksAvailabilityZone(object):
% (self.name))
+class ProfitBricksIPBlock(object):
+ """
+ Extension class which stores information about a ProfitBricks
+ IP block.
+
+ :param id: The ID of the IP block.
+ :param id: ``str``
+
+ :param name: The name of the IP block.
+ :type name: ``str``
+
+ :param href: The IP block href.
+ :type href: ``str``
+
+ :param location: The location of the IP block.
+ :type location: ``str``
+
+ :param size: Number of IP addresses in the block.
+ :type size: ``int``
+
+ :param ips: A collection of IPs associated with the block.
+ :type ips: ``list``
+
+ :param state: The current state of the IP block.
+ :type state: ``int``
+
+ :param extra: Extra properties for the IP block.
+ :type extra: ``dict``
+
+ Note: This class is ProfitBricks specific
+ """
+
+ def __init__(
+ self, id, name, href, location,
+ size, ips, state, driver,
+ extra=None
+ ):
+
+ self.id = id
+ self.name = name
+ self.href = href
+ self.location = location
+ self.size = size
+ self.ips = ips
+ self.state = state
+ self.driver = driver
+ self.extra = extra or {}
+
+ def __repr__(self):
+ return (
+ (
+ '<ProfitBricksIPBlock: id=%s,'
+ 'name=%s, href=%s,location=%s, size=%s>'
+ )
+ % (self.id, self.name, self.href, self.location, self.size)
+ )
+
+
class ProfitBricksNodeDriver(NodeDriver):
"""
Base ProfitBricks node driver.
@@ -222,26 +420,21 @@ class ProfitBricksNodeDriver(NodeDriver):
type = Provider.PROFIT_BRICKS
PROVISIONING_STATE = {
- 'INACTIVE': NodeState.PENDING,
- 'INPROCESS': NodeState.PENDING,
'AVAILABLE': NodeState.RUNNING,
- 'DELETED': NodeState.TERMINATED,
+ 'BUSY': NodeState.PENDING,
+ 'INACTIVE': NodeState.PENDING
}
NODE_STATE_MAP = {
'NOSTATE': NodeState.UNKNOWN,
'RUNNING': NodeState.RUNNING,
'BLOCKED': NodeState.STOPPED,
- 'PAUSE': NodeState.STOPPED,
- 'SHUTDOWN': NodeState.PENDING,
+ 'PAUSE': NodeState.PAUSED,
+ 'SHUTDOWN': NodeState.STOPPING,
'SHUTOFF': NodeState.STOPPED,
- 'CRASHED': NodeState.STOPPED,
- }
-
- REGIONS = {
- '1': {'region': 'us/las', 'country': 'USA'},
- '2': {'region': 'de/fra', 'country': 'DEU'},
- '3': {'region': 'de/fkb', 'country': 'DEU'},
+ 'CRASHED': NodeState.ERROR,
+ 'AVAILABLE': NodeState.RUNNING,
+ 'BUSY': NodeState.PENDING
}
AVAILABILITY_ZONE = {
@@ -320,6 +513,7 @@ class ProfitBricksNodeDriver(NodeDriver):
"""
Lists all sizes
+ :return: A list of all configurable node sizes.
:rtype: ``list`` of :class:`NodeSize`
"""
sizes = []
@@ -330,42 +524,67 @@ class ProfitBricksNodeDriver(NodeDriver):
return sizes
- def list_images(self):
+ def list_images(self, image_type=None, is_public=True):
"""
- List all images.
+ List all images with an optional filter.
- :rtype: ``list`` of :class:`NodeImage`
- """
+ :param image_type: The image type (HDD, CDROM)
+ :type image_type: ``str``
+
+ :param is_public: Image is public
+ :type is_public: ``bool``
- action = 'getAllImages'
- body = {'action': action}
+ :return: ``list`` of :class:`NodeImage`
+ :rtype: ``list``
+ """
+ response = self.connection.request(
+ action='images',
+ params={'depth': 1},
+ method='GET'
+ )
- return self._to_images(self.connection.request(action=action,
- data=body, method='POST').object)
+ return self._to_images(response.object, image_type, is_public)
def list_locations(self):
"""
List all locations.
- """
- locations = []
- for key, values in self.REGIONS.items():
- location = self._to_location(values)
- locations.append(location)
+ :return: ``list`` of :class:`NodeLocation`
+ :rtype: ``list``
+ """
+ return self._to_locations(self.connection.request(
+ action='locations',
+ params={'depth': 1},
+ method='GET').object
+ )
- return locations
+ """
+ Node functions
+ """
def list_nodes(self):
"""
List all nodes.
- :rtype: ``list`` of :class:`Node`
+ :return: ``list`` of :class:`Node`
+ :rtype: ``list``
"""
- action = 'getAllServers'
- body = {'action': action}
+ datacenters = self.ex_list_datacenters()
+ nodes = list()
+
+ for datacenter in datacenters:
+ servers_href = datacenter.extra['entities']['servers']['href']
+ response = self.connection.request(
+ action=servers_href,
+ params={'depth': 3},
+ method='GET',
+ with_full_url=True
+ )
+
+ mapped_nodes = self._to_nodes(response.object)
+ nodes += mapped_nodes
- return self._to_nodes(self.connection.request(action=action,
- data=body, method='POST').object)
+ return nodes
def reboot_node(self, node):
"""
@@ -373,20 +592,25 @@ class ProfitBricksNodeDriver(NodeDriver):
:rtype: ``bool``
"""
- action = 'resetServer'
- body = {'action': action,
- 'serverId': node.id
- }
+ action = node.extra['href'] + '/reboot'
- self.connection.request(action=action,
- data=body, method='POST').object
+ self.connection.request(
+ action=action,
+ method='POST',
+ with_full_url=True
+ )
return True
- def create_node(self, name, image, size=None, location=None,
- volume=None, ex_datacenter=None, ex_internet_access=True,
- ex_availability_zone=None, ex_ram=None, ex_cores=None,
- ex_disk=None, **kwargs):
+ def create_node(
+ self, name, image=None, size=None, location=None,
+ ex_cpu_family=None, volume=None, ex_datacenter=None,
+ ex_network_interface=True, ex_internet_access=True,
+ ex_exposed_public_ports=[], ex_exposed_private_ports=[22],
+ ex_availability_zone=None, ex_ram=None, ex_cores=None,
+ ex_disk=None, ex_password=None, ex_ssh_keys=None,
+ ex_bus_type=None, ex_disk_type=None, **kwargs
+ ):
"""
Creates a node.
@@ -394,52 +618,111 @@ class ProfitBricksNodeDriver(NodeDriver):
to the method. ProfitBricks allows you to adjust compute
resources at a much more granular level.
- :param volume: If the volume already exists then pass this in.
- :type volume: :class:`StorageVolume`
+ :param name: The name for the new node.
+ :param type: ``str``
- :param location: The location of the new data center
+ :param image: The image to create the node with.
+ :type image: :class:`NodeImage`
+
+ :param size: Standard configured size offered by
+ ProfitBricks - containing configuration for the
+ number of cpu cores, amount of ram and disk size.
+ :param size: :class:`NodeSize`
+
+ :param location: The location of the new data center
if one is not supplied.
- :type location: : :class:`NodeLocation`
+ :type location: :class:`NodeLocation`
+
+ :param ex_cpu_family: The CPU family to use (AMD_OPTERON, INTEL_XEON)
+ :type ex_cpu_family: ``str``
+
+ :param volume: If the volume already exists then pass this in.
+ :type volume: :class:`StorageVolume`
- :param ex_datacenter: If you've already created the DC then pass
+ :param ex_datacenter: If you've already created the DC then pass
it in.
- :type ex_datacenter: :class:`Datacenter`
+ :type ex_datacenter: :class:`Datacenter`
+
+ :param ex_network_interface: Create with a network interface.
+ :type ex_network_interface: : ``bool``
+
+ :param ex_internet_access: Configure public Internet access.
+ :type ex_internet_access: : ``bool``
+
+ :param ex_exposed_public_ports: Ports to be opened
+ for the public nic.
+ :param ex_exposed_public_ports: ``list`` of ``int``
+
+ :param ex_exposed_private_ports: Ports to be opened
+ for the private nic.
+ :param ex_exposed_private_ports: ``list`` of ``int``
- :param ex_internet_access: Configure public Internet access.
- :type ex_internet_access: : ``bool``
+ :param ex_availability_zone: The availability zone.
+ :type ex_availability_zone: class: `ProfitBricksAvailabilityZone`
- :param ex_availability_zone: The availability zone.
- :type ex_availability_zone: class: `ProfitBricksAvailabilityZone`
+ :param ex_ram: The amount of ram required.
+ :type ex_ram: : ``int``
- :param ex_ram: The amount of ram required.
- :type ex_ram: : ``int``
+ :param ex_cores: The number of cores required.
+ :type ex_cores: ``int``
- :param ex_cores: The number of cores required.
- :type ex_cores: : ``int``
+ :param ex_disk: The amount of disk required.
+ :type ex_disk: ``int``
- :param ex_disk: The amount of disk required.
- :type ex_disk: : ``int``
+ :param ex_password: The password for the volume.
+ :type ex_password: ``str``
+
+ :param ex_ssh_keys: Optional SSH keys for the volume.
+ :type ex_ssh_keys: ``list`` of ``str``
+
+ :param ex_bus_type: Volume bus type (VIRTIO, IDE).
+ :type ex_bus_type: ``str``
+
+ :param ex_disk_type: Volume disk type (SSD, HDD).
+ :type ex_disk_type: ``str``
:return: Instance of class ``Node``
- :rtype: :class:`Node`
+ :rtype: :class: `Node`
"""
+
+ """
+ If we have a volume we can determine the DC
+ that it belongs to and set accordingly.
+ """
+ if volume is not None:
+ dc_url_pruned = volume.extra['href'].split('/')[:-2]
+ dc_url = '/'.join(item for item in dc_url_pruned)
+ ex_datacenter = self.ex_describe_datacenter(
+ ex_href=dc_url
+ )
+
if not ex_datacenter:
'''
- We generate a name from the server name passed into the function.
+ Determine location for new DC by
+ getting the location of the image.
'''
+ if not location:
+ if image is not None:
+ location = self.ex_describe_location(
+ ex_location_id=image.extra['location']
+ )
- 'Creating a Datacenter for the node since one was not provided.'
+ '''
+ Creating a Datacenter for the node
+ since one was not provided.
+ '''
new_datacenter = self._create_new_datacenter_for_node(
name=name,
location=location
)
- datacenter_id = new_datacenter.id
- 'Waiting for the Datacenter create operation to finish.'
- self._wait_for_datacenter_state(datacenter=new_datacenter)
- else:
- datacenter_id = ex_datacenter.id
- new_datacenter = None
+ '''
+ Then wait for the operation to finish,
+ assigning the full data center on completion.
+ '''
+ ex_datacenter = self._wait_for_datacenter_state(
+ datacenter=new_datacenter
+ )
if not size:
if not ex_ram:
@@ -451,7 +734,20 @@ class ProfitBricksNodeDriver(NodeDriver):
'NodeSize or specify ex_cores as '
'an extra parameter.')
- if not volume:
+ '''
+ If passing in an image we need
+ to enfore a password or ssh keys.
+ '''
+ if not volume and image is not None:
+ if ex_password is None and ex_ssh_keys is None:
+ raise ValueError(
+ (
+ 'When creating a server without a '
+ 'volume, you need to specify either an '
+ 'array of SSH keys or a volume password.'
+ )
+ )
+
if not size:
if not ex_disk:
raise ValueError('You need to either pass a '
@@ -464,8 +760,15 @@ class ProfitBricksNodeDriver(NodeDriver):
for your specific use.
'''
- if not ex_disk:
- ex_disk = size.disk
+ if image is not None:
+ if not ex_disk:
+ ex_disk = size.disk
+
+ if not ex_disk_type:
+ ex_disk_type = 'HDD'
+
+ if not ex_bus_type:
+ ex_bus_type = 'VIRTIO'
if not ex_ram:
ex_ram = size.ram
@@ -473,62 +776,148 @@ class ProfitBricksNodeDriver(NodeDriver):
if not ex_cores:
ex_cores = size.extra['cores']
- '''
- A pasword is automatically generated if it is
- not provided. This is then sent via email to
- the admin contact on record.
- '''
-
- if 'auth' in kwargs:
- auth = self._get_and_check_auth(kwargs["auth"])
- password = auth.password
- else:
- password = None
+ action = ex_datacenter.href + '/servers'
+ body = {
+ 'properties': {
+ 'name': name,
+ 'ram': ex_ram,
+ 'cores': ex_cores
+ },
+ 'entities': {
+ 'volumes': {
+ 'items': []
+ }
+ }
+ }
'''
- Create a StorageVolume that can be attached to the
- server when it is created.
+ If we are using a pre-existing storage volume.
'''
- if not volume:
- volume = self._create_node_volume(ex_disk=ex_disk,
- image=image,
- password=password,
- name=name,
- ex_datacenter=ex_datacenter,
- new_datacenter=new_datacenter)
-
- storage_id = volume.id
-
- 'Waiting on the storage volume to be created before provisioning '
- 'the instance.'
- self._wait_for_storage_volume_state(volume)
- else:
- if ex_datacenter:
- datacenter_id = ex_datacenter.id
- else:
- datacenter_id = volume.extra['datacenter_id']
-
- storage_id = volume.id
-
- action = 'createServer'
- body = {'action': action,
- 'request': 'true',
- 'serverName': name,
- 'cores': str(ex_cores),
- 'ram': str(ex_ram),
- 'bootFromStorageId': storage_id,
- 'internetAccess': str(ex_internet_access).lower(),
- 'dataCenterId': datacenter_id
+ if volume is not None:
+ body['entities']['volumes']['items'].append({'id': volume.id})
+ elif image is not None:
+ new_volume = {
+ 'properties': {
+ 'size': ex_disk,
+ 'name': name + ' - volume',
+ 'image': image.id,
+ 'type': ex_disk_type,
+ 'bus': ex_bus_type
}
+ }
+
+ if ex_password is not None:
+ new_volume['properties']['imagePassword'] = ex_password
+
+ if ex_ssh_keys is not None:
+ new_volume['properties']['sshKeys'] = ex_ssh_keys
+
+ body['entities']['volumes']['items'].append(new_volume)
+
+ if ex_network_interface is True:
+ body['entities']['nics'] = {}
+ body['entities']['nics']['items'] = list()
- if ex_availability_zone:
- body['availabilityZone'] = ex_availability_zone.name
+ '''
+ Get the LANs for the data center this node
+ will be provisioned at.
+ '''
+ dc_lans = self.ex_list_lans(
+ datacenter=ex_datacenter
+ )
+
+ private_lans = [lan for lan in dc_lans if lan.is_public is False]
+ private_lan = None
+
+ if private_lans:
+ private_lan = private_lans[0]
+
+ if private_lan is not None:
+ private_nic = {
+ 'properties': {
+ 'name': name + ' - private nic',
+ 'lan': private_lan.id,
+ },
+ 'entities': {
+ 'firewallrules': {
+ 'items': []
+ }
+ }
+ }
- data = self.connection.request(action=action,
- data=body,
- method='POST').object
- nodes = self._to_nodes(data)
- return nodes[0]
+ for port in ex_exposed_private_ports:
+ private_nic['entities']['firewallrules']['items'].append(
+ {
+ 'properties': {
+ 'name': (
+ '{name} - firewall rule:{port}'.format(
+ name=name, port=port
+ )
+ ),
+ 'protocol': 'TCP',
+ 'portRangeStart': port,
+ 'portRangeEnd': port
+ }
+ }
+ )
+
+ body['entities']['nics']['items'].append(private_nic)
+
+ if ex_internet_access is not None and ex_internet_access is True:
+ public_lans = [lan for lan in dc_lans if lan.is_public]
+ public_lan = None
+
+ if public_lans:
+ public_lan = public_lans[0]
+
+ if public_lan is not None:
+ pub_nic = {
+ 'properties': {
+ 'name': name + ' - public nic',
+ 'lan': public_lan.id,
+ },
+ 'entities': {
+ 'firewallrules': {
+ 'items': []
+ }
+ }
+ }
+
+ for port in ex_exposed_public_ports:
+ pub_nic['entities']['firewallrules']['items'].append(
+ {
+ 'properties': {
+ 'name': (
+ '{name} - firewall rule:{port}'.format(
+ name=name, port=port
+ )
+ ),
+ 'protocol': 'TCP',
+ 'portRangeStart': port,
+ 'portRangeEnd': port
+ }
+ }
+ )
+
+ body['entities']['nics']['items'].append(pub_nic)
+
+ if ex_cpu_family is not None:
+ body['properties']['cpuFamily'] = ex_cpu_family
+
+ if ex_availability_zone is not None:
+ body['properties']['availabilityZone'] = ex_availability_zone.name
+
+ response = self.connection.request(
+ action=action,
+ headers={
+ 'Content-Type': 'application/json'
+ },
+ data=body,
+ method='POST',
+ with_full_url=True
+ )
+
+ return self._to_node(response.object, response.headers)
def destroy_node(self, node, ex_remove_attached_disks=False):
"""
@@ -542,13 +931,18 @@ class ProfitBricksNodeDriver(NodeDriver):
:rtype: : ``bool``
"""
- action = 'deleteServer'
- body = {'action': action,
- 'serverId': node.id
- }
- self.connection.request(action=action,
- data=body, method='POST').object
+ if ex_remove_attached_disks is True:
+ for volume in self.ex_list_attached_volumes(node):
+ self.destroy_volume(volume)
+
+ action = node.extra['href']
+
+ self.connection.request(
+ action=action,
+ method='DELETE',
+ with_full_url=True
+ )
return True
@@ -558,104 +952,190 @@ class ProfitBricksNodeDriver(NodeDriver):
def list_volumes(self):
"""
- Lists all voumes.
+ List all volumes attached to a data center.
+
+ :return: ``list`` of :class:`StorageVolume`
+ :rtype: ``list``
"""
- action = 'getAllStorages'
- body = {'action': action}
+ datacenters = self.ex_list_datacenters()
+ volumes = list()
- return self._to_volumes(self.connection.request(action=action,
- data=body,
- method='POST').object)
+ for datacenter in datacenters:
+ volumes_href = datacenter.extra['entities']['volumes']['href']
- def attach_volume(self, node, volume, device=None, ex_bus_type=None):
- """
- Attaches a volume.
+ response = self.connection.request(
+ action=volumes_href,
+ params={'depth': 3},
+ method='GET',
+ with_full_url=True
+ )
- :param volume: The volume you're attaching.
- :type volume: :class:`StorageVolume`
+ mapped_volumes = self._to_volumes(response.object)
+ volumes += mapped_volumes
+
+ return volumes
- :param node: The node to which you're attaching the volume.
- :type node: :class:`Node`
+ def attach_volume(self, node, volume):
+ """
+ Attaches a volume.
- :param device: The device number order.
- :type device: : ``int``
+ :param node: The node to which you're attaching the volume.
+ :type node: :class:`Node`
- :param ex_bus_type: Bus type. Either IDE or VIRTIO (default).
- :type ex_bus_type: ``str``
+ :param volume: The volume you're attaching.
+ :type volume: :class:`StorageVolume`
:return: Instance of class ``StorageVolume``
:rtype: :class:`StorageVolume`
"""
- action = 'connectStorageToServer'
- body = {'action': action,
- 'request': 'true',
- 'storageId': volume.id,
- 'serverId': node.id,
- 'busType': ex_bus_type,
- 'deviceNumber': str(device)
- }
-
- self.connection.request(action=action,
- data=body, method='POST').object
- return volume
+ action = node.extra['href'] + '/volumes'
+ body = {
+ 'id': volume.id
+ }
- def create_volume(self, size, name=None,
- ex_datacenter=None, ex_image=None, ex_password=None):
+ data = self.connection.request(
+ action=action,
+ headers={
+ 'Content-Type': 'application/json'
+ },
+ data=body,
+ method='POST',
+ with_full_url=True
+ )
+
+ return self._to_volume(data.object, data.headers)
+
+ def create_volume(
+ self,
+ size,
+ image,
+ ex_datacenter,
+ name=None,
+ ex_type=None,
+ ex_bus_type=None,
+ ex_ssh_keys=None,
+ ex_password=None,
+ ex_availability_zone=None
+ ):
"""
Creates a volume.
- :param ex_datacenter: The datacenter you're placing
+ :param size: The size of the volume in GB.
+ :type size: ``int``
+
+ :param image: The OS image for the volume.
+ :type image: :class:`NodeImage`
+
+ :param ex_datacenter: The datacenter you're placing
the storage in. (req)
- :type ex_datacenter: :class:`Datacenter`
+ :type ex_datacenter: :class:`Datacenter`
+
+ :param name: The name to be given to the volume.
+ :param name: ``str``
+
+ :param ex_type: The type to be given to the volume (SSD or HDD).
+ :param ex_type: ``str``
+
+ :param ex_bus_type: Bus type. Either IDE or VIRTIO (default).
+ :type ex_bus_type: ``str``
- :param ex_image: The OS image for the volume.
- :type ex_image: :class:`NodeImage`
+ :param ex_ssh_keys: Optional SSH keys.
+ :type ex_ssh_keys: ``dict``
- :param ex_password: Optional password for root.
- :type ex_password: : ``str``
+ :param ex_password: Optional password for root.
+ :type ex_password: ``str``
+
+ :param ex_availability_zone: Volume Availability Zone.
+ :type ex_availability_zone: ``str``
:return: Instance of class ``StorageVolume``
:rtype: :class:`StorageVolume`
"""
- action = 'createStorage'
- body = {'action': action,
- 'request': 'true',
- 'size': str(size),
- 'storageName': name,
- 'mountImageId': ex_image.id
- }
- if ex_datacenter:
- body['dataCenterId'] = ex_datacenter.id
-
- if ex_password:
- body['profitBricksImagePassword'] = ex_password
+ if not ex_datacenter:
+ raise ValueError('You need to specify a data center'
+ ' to attach this volume to.')
+
+ if not image:
+ raise ValueError('You need to specify an image'
+ ' to create this volume from.')
+
+ if image.extra['image_type'] != 'HDD':
+ raise ValueError('Invalid type of {image_type} specified for '
+ '{image_name}, which needs to be of type HDD'
+ .format(image_type=image.extra['image_type'],
+ image_name=image.name))
+
+ if ex_datacenter.extra['location'] != image.extra['location']:
+ raise ValueError(
+ 'The image {image_name} '
+ '(location: {image_location}) you specified '
+ 'is not available at the data center '
+ '{datacenter_name} '
+ '(location: {datacenter_location}).'
+ .format(
+ image_name=image.extra['name'],
+ datacenter_name=ex_datacenter.extra['name'],
+ image_location=image.extra['location'],
+ datacenter_location=ex_datacenter.extra['location']
+ )
+ )
- data = self.connection.request(action=action,
- data=body,
- method='POST').object
- volumes = self._to_volumes(data)
- return volumes[0]
+ action = ex_datacenter.href + '/volumes'
+ body = {
+ 'properties': {
+ 'size': size,
+ 'image': image.id
+ }
+ }
- def detach_volume(self, volume):
+ if name is not None:
+ body['properties']['name'] = name
+ if ex_type is not None:
+ body['properties']['type'] = ex_type
+ if ex_bus_type is not None:
+ body['properties']['bus'] = ex_bus_type
+ if ex_ssh_keys is not None:
+ body['properties']['sshKeys'] = ex_ssh_keys
+ if ex_password is not None:
+ body['properties']['imagePassword'] = ex_password
+ if ex_availability_zone is not None:
+ body['properties']['availabilityZone'] = ex_availability_zone
+
+ response = self.connection.request(
+ action=action,
+ headers={
+ 'Content-Type': 'application/json'
+ },
+ data=body,
+ method='POST',
+ with_full_url=True
+ )
+
+ return self._to_volume(response.object, response.headers)
+
+ def detach_volume(self, node, volume):
"""
Detaches a volume.
+ :param node: The node to which you're detaching the volume.
+ :type node: :class:`Node`
+
:param volume: The volume you're detaching.
:type volume: :class:`StorageVolume`
:rtype: :``bool``
"""
- node_id = volume.extra['server_id']
- action = 'disconnectStorageFromServer'
- body = {'action': action,
- 'storageId': volume.id,
- 'serverId': node_id
- }
+ action = node.extra['href'] + '/volumes/{volume_id}'.format(
+ volume_id=volume.id
+ )
- self.connection.request(action=action,
- data=body, method='POST').object
+ self.connection.request(
+ action=action,
+ method='DELETE',
+ with_full_url=True
+ )
return True
@@ -663,121 +1143,139 @@ class ProfitBricksNodeDriver(NodeDriver):
"""
Destroys a volume.
- :param volume: The volume you're attaching.
+ :param volume: The volume you're destroying.
:type volume: :class:`StorageVolume`
:rtype: : ``bool``
"""
- action = 'deleteStorage'
- body = {'action': action,
- 'storageId': volume.id}
+ action = volume.extra['href']
- self.connection.request(action=action,
- data=body, method='POST').object
+ self.connection.request(
+ action=action,
+ method='DELETE',
+ with_full_url=True
+ )
return True
- def ex_update_volume(self, volume, storage_name=None, size=None):
+ """
+ Volume snapshot functions
+ """
+
+ def list_snapshots(self):
"""
- Updates a volume.
+ Fetches as a list of all snapshots
- :param volume: The volume you're attaching..
- :type volume: :class:`StorageVolume`
+ :return: ``list`` of class ``VolumeSnapshot``
+ :rtype: `list`
+ """
- :param storage_name: The name of the volume.
- :type storage_name: : ``str``
+ response = self.connection.request(
+ action='/snapshots',
+ params={'depth': 3},
+ method='GET'
+ )
- :param size: The desired size.
- :type size: ``int``
+ return self._to_snapshots(response.object)
- :rtype: : ``bool``
+ def create_volume_snapshot(self, volume):
"""
- action = 'updateStorage'
- body = {'action': action,
- 'request': 'true',
- 'storageId': volume.id
- }
+ Creates a snapshot for a volume
- if storage_name:
- body['storageName'] = storage_name
- if size:
- body['size'] = str(size)
+ :param volume: The volume you're creating a snapshot for.
+ :type volume: :class:`StorageVolume`
- self.connection.request(action=action,
- data=body, method='POST').object
+ :return: Instance of class ``VolumeSnapshot``
+ :rtype: :class:`VolumeSnapshot`
+ """
- return True
+ action = volume.extra['href'] + '/create-snapshot'
+
+ response = self.connection.request(
+ action=action,
+ headers={
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ method='POST',
+ with_full_url=True
+ )
+
+ return self._to_snapshot(response.object, response.headers)
- def ex_describe_volume(self, volume_id):
+ def destroy_volume_snapshot(self, snapshot):
"""
- Describes a volume.
+ Delete a snapshot
- :param volume_id: The ID of the volume you're describing.
- :type volume_id: :class:`StorageVolume`
+ :param snapshot: The snapshot you wish to delete.
+ :type: snapshot: :class:`VolumeSnapshot`
- :return: Instance of class ``StorageVolume``
- :rtype: :class:`StorageVolume`
+ :rtype ``bool``
"""
- action = 'getStorage'
- body = {'action': action,
- 'storageId': volume_id
- }
- data = self.connection.request(action=action,
- data=body,
- method='POST').object
- volumes = self._to_volumes(data)
- return volumes[0]
+ action = snapshot.extra['href']
+
+ self.connection.request(
+ action=action,
+ method='DELETE',
+ with_full_url=True
+ )
+
+ return True
"""
Extension Functions
"""
- ''' Server Extension Functions
- '''
+ """
+ Server Extension Functions
+ """
+
def ex_stop_node(self, node):
"""
Stops a node.
This also deallocates the public IP space.
- :param node: The node you wish to halt.
- :type node: :class:`Node`
+ :param node: The node you wish to halt.
+ :type node: :class:`Node`
:rtype: : ``bool``
"""
- action = 'stopServer'
- body = {'action': action,
- 'serverId': node.id
- }
+ action = node.extra['href'] + '/stop'
- self.connection.request(action=action,
- data=body, method='POST').object
+ self.connection.request(
+ action=action,
+ method='POST',
+ with_full_url=True
+ )
return True
def ex_start_node(self, node):
"""
- Starts a volume.
+ Starts a node.
- :param node: The node you wish to start.
- :type node: :class:`Node`
+ :param node: The node you wish to start.
+ :type node: :class:`Node`
- :rtype: : ``bool``
+ :rtype: ``bool``
"""
- action = 'startServer'
- body = {'action': action,
- 'serverId': node.id
- }
+ action = node.extra['href'] + '/start'
- self.connection.request(action=action,
- data=body, method='POST').object
+ self.connection.request(
+ action=action,
+ method='POST',
+ with_full_url=True
+ )
return True
def ex_list_availability_zones(self):
"""
Returns a list of availability zones.
+
+ :return: ``list`` of :class:`ProfitBricksAvailabilityZone`
+ :rtype: ``list``
"""
availability_zones = []
@@ -792,100 +1290,219 @@ class ProfitBricksNodeDriver(NodeDriver):
return availability_zones
- def ex_describe_node(self, node):
+ def ex_list_attached_volumes(self, node):
"""
- Describes a node.
+ Returns a list of attached volumes for a server
- :param node: The node you wish to describe.
- :type node: :class:`Node`
+ :param node: The node with the attached volumes.
+ :type node: :class:`Node`
- :return: Instance of class ``Node``
- :rtype: :class:`Node`
+ :return: ``list`` of :class:`StorageVolume`
+ :rtype: ``list``
"""
- action = 'getServer'
- body = {'action': action,
- 'serverId': node.id
- }
+ action = node.extra['entities']['volumes']['href']
+ response = self.connection.request(
+ action=action,
+ params={'depth': 3},
+ method='GET',
+ with_full_url=True
+ )
- data = self.connection.request(action=action,
- data=body,
- method='POST').object
- nodes = self._to_nodes(data)
- return nodes[0]
+ return self._to_volumes(response.object)
- def ex_update_node(self, node, name=None, cores=None,
- ram=None, availability_zone=None):
+ def ex_describe_node(
+ self,
+ ex_href=None,
+ ex_datacenter_id=None,
+ ex_node_id=None
+ ):
"""
- Updates a node.
+ Fetches a node directly by href or
+ by a combination of the datacenter
+ ID and the server ID.
- :param cores: The number of CPUs the node should have.
- :type device: : ``int``
+ :param ex_href: The href (url) of the node you wish to describe.
+ :type ex_href: ``str``
- :param ram: The amount of ram the machine should have.
- :type ram: : ``int``
+ :param ex_datacenter_id: The ID for the data center.
+ :type ex_datacenter_id: ``str``
- :param ex_availability_zone: Update the availability zone.
- :type ex_availability_zone: :class:`ProfitBricksAvailabilityZone`
+ :param ex_node_id: The ID for the node (server).
+ :type ex_node_id: ``str``
- :rtype: : ``bool``
+ :return: Instance of class ``Node``
+ :rtype: :class:`Node`
"""
- action = 'updateServer'
- body = {'action': action,
- 'request': 'true',
- 'serverId': node.id
- }
+ use_full_url = True
- if name:
- body['serverName'] = name
+ if ex_href is None:
+ if ex_datacenter_id is None or ex_node_id is None:
+ raise ValueError(
+ 'IDs for the data center and node are required.'
+ )
+ else:
+ use_full_url = False
+ ex_href = (
+ 'datacenters/{datacenter_id}/'
+ 'servers/{server_id}'
+ ).format(
+ datacenter_id=ex_datacenter_id,
+ server_id=ex_node_id
+ )
+
+ response = self.connection.request(
+ action=ex_href,
+ method='GET',
+ params={'depth': 3},
+ with_full_url=use_full_url
+ )
+
+ return self._to_node(response.object)
- if cores:
- body['cores'] = str(cores)
+ def ex_update_node(self, node, name=None, cores=None,
+ ram=None, availability_zone=None,
+ ex_licence_type=None, ex_boot_volume=None,
+ ex_boot_cdrom=None, ex_cpu_family=None):
+ """
+ Updates a node.
- if ram:
- body['ram'] = str(ram)
+ :param node: The node you wish to update.
+ :type node: :class:`Node`
- if availability_zone:
- body['availabilityZone'] = availability_zone.name
+ :param name: The new name for the node.
+ :type name: ``str``
- self.connection.request(action=action,
- data=body, method='POST').object
+ :param cores: The number of CPUs the node should have.
+ :type cores: : ``int``
- return True
+ :param ram: The amount of ram the node should have.
+ :type ram: : ``int``
- '''
- Datacenter Extension Functions
- '''
+ :param availability_zone: Update the availability zone.
+ :type availability_zone: :class:`ProfitBricksAvailabilityZone`
- def ex_create_datacenter(self, name, location):
- """
- Creates a datacenter.
+ :param ex_licence_type: Licence type (WINDOWS, LINUX, OTHER).
+ :type ex_licence_type: ``str``
- ProfitBricks has a concept of datacenters.
- These represent buckets into which you
- can place various compute resources.
+ :param ex_boot_volume: Setting the new boot (HDD) volume.
+ :type ex_boot_volume: :class:`StorageVolume`
- :param name: The DC name.
- :type name: : ``str``
+ :param ex_boot_cdrom: Setting the new boot (CDROM) volume.
+ :type ex_boot_cdrom: :class:`StorageVolume`
- :param location: The DC region.
- :type location: : ``str``
+ :param ex_cpu_family: CPU family (INTEL_XEON, AMD_OPTERON).
+ :type ex_cpu_family: ``str``
- :return: Instance of class ``Datacenter``
- :rtype: :class:`Datacenter`
+ :return: Instance of class ``Node``
+ :rtype: :class: `Node`
+ """
+ action = node.extra['href']
+ body = {}
+
+ if name is not None:
+ body['name'] = name
+
+ if cores is not None:
+ body['cores'] = cores
+
+ if ram is not None:
+ body['ram'] = ram
+
+ if availability_zone is not None:
+ body['availabilityZone'] = availability_zone.name
+
+ if ex_licence_type is not None:
+ body['licencetype'] = ex_licence_type
+
+ if ex_boot_volume is not None:
+ body['bootVolume'] = ex_boot_volume.id
+
+ if ex_boot_cdrom is not None:
+ body['bootCdrom'] = ex_boot_cdrom.id
+
+ if ex_cpu_family is not None:
+ body['allowReboot'] = True
+ body['cpuFamily'] = ex_cpu_family
+
+ response = self.connection.request(
+ action=action,
+ data=body,
+ headers={
+ 'Content-Type':
+ 'application/json'
+ },
+ method='PATCH',
+ with_full_url=True
+ )
+
+ return self._to_node(response.object, response.headers)
+
+ """
+ Data center Extension Functions
+ """
+
+ def ex_create_datacenter(
+ self,
+ name,
+ location,
+ description=None
+ ):
+ """
+ Creates a datacenter.
+
+ ProfitBricks has a concept of datacenters.
+ These represent buckets into which you
+ can place various compute resources.
+
+ :param name: The datacenter name.
+ :type name: : ``str``
+
+ :param location: instance of class ``NodeLocation``.
+ :type location: : ``NodeLocation``
+
+ :param description: The datacenter description.
+ :type description: : ``str``
+
+ :return: Instance of class ``Datacenter``
+ :rtype: :class:`Datacenter`
"""
- action = 'createDataCenter'
+ body = {
+ 'properties': {
+ 'name': name,
+ 'location': location.id
+ }
+ }
- body = {'action': action,
- 'request': 'true',
- 'dataCenterName': name,
- 'location': location.lower()
+ if description is not None:
+ body['properties']['description'] = description
+
+ body['entities'] = defaultdict(dict)
+ body['entities']['lans']['items'] = [
+ {
+ 'properties': {
+ 'name': name + ' - public lan',
+ 'public': True
+ }
+ },
+ {
+ 'properties': {
+ 'name': name + ' - private lan',
+ 'public': False
}
- data = self.connection.request(action=action,
- data=body,
- method='POST').object
- datacenters = self._to_datacenters(data)
- return datacenters[0]
+ }
+ ]
+
+ response = self.connection.request(
+ action='datacenters',
+ headers={
+ 'Content-Type': 'application/json'
+ },
+ data=body,
+ method='POST'
+ )
+
+ return self._to_datacenter(response.object, response.headers)
def ex_destroy_datacenter(self, datacenter):
"""
@@ -896,141 +1513,366 @@ class ProfitBricksNodeDriver(NodeDriver):
:rtype: : ``bool``
"""
- action = 'deleteDataCenter'
- body = {'action': action,
- 'dataCenterId': datacenter.id
- }
+ action = datacenter.href
- self.connection.request(action=action,
- data=body, method='POST').object
+ self.connection.request(
+ action=action,
+ method='DELETE',
+ with_full_url=True
+ )
return True
- def ex_describe_datacenter(self, datacenter_id):
+ def ex_describe_datacenter(self, ex_href=None, ex_datacenter_id=None):
"""
- Describes a datacenter.
+ Fetches the details for a data center.
+
+ :param ex_href: The href for the data center
+ you are describing.
+ :type ex_href: ``str``
- :param datacenter_id: The DC you are describing.
- :type datacenter_id: ``str``
+ :param ex_datacenter_id: The ID for the data cente
+ you are describing.
+ :type ex_datacenter_id: ``str``
:return: Instance of class ``Datacenter``
:rtype: :class:`Datacenter`
"""
- action = 'getDataCenter'
- body = {'action': action,
- 'dataCenterId': datacenter_id
- }
+ use_full_url = True
- data = self.connection.request(action=action,
- data=body,
- method='POST').object
- datacenters = self._to_datacenters(data)
- return datacenters[0]
+ if ex_href is None:
+ if ex_datacenter_id is None:
+ raise ValueError(
+ 'The data center ID is required.'
+ )
+ else:
+ use_full_url = False
+ ex_href = (
+ 'datacenters/{datacenter_id}'
+ ).format(
+ datacenter_id=ex_datacenter_id
+ )
+
+ response = self.connection.request(
+ action=ex_href,
+ method='GET',
+ params={'depth': 3},
+ with_full_url=use_full_url
+ )
+
+ return self._to_datacenter(response.object)
def ex_list_datacenters(self):
"""
Lists all datacenters.
- :return: ``list`` of class ``Datacenter``
- :rtype: :class:`Datacenter`
+ :return: ``list`` of :class:`DataCenter`
+ :rtype: ``list``
"""
- action = 'getAllDataCenters'
- body = {'action': action}
+ response = self.connection.request(
+ action='datacenters',
+ params={'depth': 2},
+ method='GET'
+ )
- return self._to_datacenters(self.connection.request(
- action=action,
- data=body,
- method='POST').object)
+ return self._to_datacenters(response.object)
def ex_rename_datacenter(self, datacenter, name):
"""
Update a datacenter.
- :param datacenter: The DC you are renaming.
- :type datacenter: :class:`Datacenter`
+ :param datacenter: The DC you are renaming.
+ :type datacenter: :class:`Datacenter`
- :param name: The DC name.
- :type name: : ``str``
+ :param name: The DC name.
+ :type name: : ``str``
- :rtype: : ``bool``
+ :return: Instance of class ``Datacenter``
+ :rtype: :class:`Datacenter`
"""
- action = 'updateDataCenter'
- body = {'action': action,
- 'request': 'true',
- 'dataCenterId': datacenter.id,
- 'dataCenterName': name
- }
+ action = datacenter.href
+ body = {
+ 'name': name
+ }
- self.connection.request(action=action,
- data=body,
- method='POST').object
+ response = self.connection.request(
+ action=action,
+ headers={
+ 'Content-Type':
+ 'application/json'
+ },
+ data=body,
+ method='PATCH',
+ with_full_url=True
+ )
- return True
+ return self._to_datacenter(response.object, response.headers)
+
+ """
+ Image Extension Functions
+ """
- def ex_clear_datacenter(self, datacenter):
+ def ex_describe_image(self, ex_href=None, ex_image_id=None):
"""
- Clear a datacenter.
+ Describe a ProfitBricks image
- This removes all objects in a DC.
+ :param ex_href: The href for the image you are describing
+ :type ex_href: ``str``
- :param datacenter: The DC you're clearing.
- :type datacenter: :class:`Datacenter`
+ :param ex_image_id: The ID for the image you are describing
+ :type ex_image_id: ``str``
+
+ :return: Instance of class ``Image``
+ :rtype: :class:`Image`
+ """
+
+ use_full_url = True
+
+ if ex_href is None:
+ if ex_image_id is None:
+ raise ValueError(
+ 'The image ID is required.'
+ )
+ else:
+ use_full_url = False
+ ex_href = (
+ 'images/{image_id}'
+ ).format(
+ image_id=ex_image_id
+ )
+
+ response = self.connection.request(
+ action=ex_href,
+ method='GET',
+ with_full_url=use_full_url
+ )
+
+ return self._to_image(response.object)
+
+ def ex_delete_image(self, image):
+ """
+ Delete a private image
+
+ :param image: The private image you are deleting.
+ :type image: :class:`NodeImage`
:rtype: : ``bool``
"""
- action = 'clearDataCenter'
- body = {'action': action,
- 'dataCenterId': datacenter.id
- }
- self.connection.request(action=action,
- data=body, method='POST').object
+ self.connection.request(
+ action=image.extra['href'],
+ method='DELETE',
+ with_full_url=True
+ )
return True
- '''
+ def ex_update_image(
+ self, image, name=None, description=None,
+ licence_type=None, cpu_hot_plug=None,
+ cpu_hot_unplug=None, ram_hot_plug=None,
+ ram_hot_unplug=None, nic_hot_plug=None,
+ nic_hot_unplug=None, disc_virtio_hot_plug=None,
+ disc_virtio_hot_unplug=None, disc_scsi_hot_plug=None,
+ disc_scsi_hot_unplug=None
+ ):
+ """
+ Update a private image
+
+ :param image: The private image you are deleting.
+ :type image: :class:`NodeImage`
+
+ :return: Instance of class ``Image``
+ :rtype: :class:`Image`
+ """
+ action = image.extra['href']
+ body = {}
+
+ if name is not None:
+ body['name'] = name
+
+ if description is not None:
+ body['description'] = description
+
+ if licence_type is not None:
+ body['licence_type'] = licence_type
+
+ if cpu_hot_plug is not None:
+ body['cpu_hot_plug'] = cpu_hot_plug
+
+ if cpu_hot_unplug is not None:
+ body['cpu_hot_unplug'] = cpu_hot_unplug
+
+ if ram_hot_plug is not None:
+ body['ram_hot_plug'] = ram_hot_plug
+
+ if ram_hot_unplug is not None:
+ body['ram_hot_unplug'] = ram_hot_unplug
+
+ if nic_hot_plug is not None:
+ body['nic_hot_plug'] = nic_hot_plug
+
+ if nic_hot_unplug is not None:
+ body['nic_hot_unplug'] = nic_hot_unplug
+
+ if disc_virtio_hot_plug is not None:
+ body['disc_virtio_hot_plug'] = disc_virtio_hot_plug
+
+ if disc_virtio_hot_unplug is not None:
+ body['disc_virtio_hot_unplug'] = disc_virtio_hot_unplug
+
+ if disc_scsi_hot_plug is not None:
+ body['disc_scsi_hot_plug'] = disc_scsi_hot_plug
+
+ if disc_scsi_hot_unplug is not None:
+ body['disc_scsi_hot_unplug'] = disc_scsi_hot_unplug
+
+ response = self.connection.request(
+ action=action,
+ headers={
+ 'Content-type':
+ 'application/json'
+ },
+ data=body,
+ method='PATCH',
+ with_full_url=True
+ )
+
+ return self._to_image(response.object, response.headers)
+
+ """
+ Location Extension Functions
+ """
+
+ def ex_describe_location(self, ex_href=None, ex_location_id=None):
+ """
+ Fetch details for a ProfitBricks location.
+
+ :param ex_href: The href for the location
+ you are describing.
+ :type ex_href: ``str``
+
+ :param ex_location_id: The id for the location you are
+ describing ('de/fra', 'de/fkb', 'us/las')
+ :type ex_location_id: ``str``
+
+ :return: Instance of class ``NodeLocation``
+ :rtype: :class:`NodeLocation`
+ """
+
+ use_full_url = True
+
+ if ex_href is None:
+ if ex_location_id is None:
+ raise ValueError(
+ 'The loctation ID is required.'
+ )
+ else:
+ use_full_url = False
+ ex_href = (
+ 'locations/{location_id}'
+ ).format(
+ location_id=ex_location_id
+ )
+
+ response = self.connection.request(
+ action=ex_href,
+ method='GET',
+ with_full_url=use_full_url
+ )
+
+ return self._to_location(response.object)
+
+ """
Network Interface Extension Functions
- '''
+ """
def ex_list_network_interfaces(self):
"""
- Lists all network interfaces.
+ Fetch a list of all network interfaces from all data centers.
:return: ``list`` of class ``ProfitBricksNetworkInterface``
- :rtype: :class:`ProfitBricksNetworkInterface`
+ :rtype: `list`
"""
- action = 'getAllNic'
- body = {'action': action}
+ nodes = self.list_nodes()
+ nics = list()
- return self._to_interfaces(
- self.connection.request(action=action,
- data=body,
- method='POST').object)
+ for node in nodes:
+ action = node.extra['entities']['nics']['href']
+ nics += self._to_interfaces(
+ self.connection.request(
+ action=action,
+ params={'depth': 1},
+ method='GET',
+ with_full_url=True
+ ).object)
- def ex_describe_network_interface(self, network_interface):
+ return nics
+
+ def ex_describe_network_interface(
+ self,
+ ex_href=None,
+ ex_datacenter_id=None,
+ ex_server_id=None,
+ ex_nic_id=None
+ ):
"""
- Describes a network interface.
+ Fetch information on a network interface.
- :param network_interface: The NIC you wish to describe.
- :type network_interface: :class:`ProfitBricksNetworkInterface`
+ :param ex_href: The href of the NIC you wish to describe.
+ :type ex_href: ``str``
+
+ :param ex_datacenter_id: The ID of parent data center
+ of the NIC you wish to describe.
+ :type ex_datacenter_id: ``str``
+
+ :param ex_server_id: The server the NIC is connected to.
+ :type ex_server_id: ``str``
+
+ :param ex_nic_id: The ID of the NIC
+ :type ex_nic_id: ``str``
:return: Instance of class ``ProfitBricksNetworkInterface``
:rtype: :class:`ProfitBricksNetworkInterface`
"""
- action = 'getNic'
- body = {'action': action,
- 'nicId': network_interface.id
- }
- return self._to_interface(
- self.connection.request(
- action=action,
- data=body,
- method='POST').object.findall('.//return')[0])
+ use_full_url = True
+
+ if ex_href is None:
+ if (
+ ex_datacenter_id is None or
+ ex_server_id is None or
+ ex_nic_id is None
+ ):
+ raise ValueError(
+ (
+ 'IDs are required for the data center',
+ 'server and network interface.'
+ )
+ )
+ else:
+ use_full_url = False
+ ex_href = (
+ 'datacenters/{datacenter_id}'
+ '/servers/{server_id}'
+ '/nics/{nic_id}'
+ ).format(
+ datacenter_id=ex_datacenter_id,
+ server_id=ex_server_id,
+ nic_id=ex_nic_id
+ )
+
+ response = self.connection.request(
+ action=ex_href,
+ method='GET',
+ with_full_url=use_full_url
+ )
+
+ return self._to_interface(response.object)
def ex_create_network_interface(self, node,
- lan_id=None, ip=None, nic_name=None,
+ lan_id=None, ips=None, nic_name=None,
dhcp_active=True):
"""
Creates a network interface.
@@ -1038,8 +1880,8 @@ class ProfitBricksNodeDriver(NodeDriver):
:param lan_id: The ID for the LAN.
:type lan_id: : ``int``
- :param ip: The IP address for the NIC.
- :type ip: ``str``
+ :param ips: The IP addresses for the NIC.
+ :type ips: ``list``
:param nic_name: The name of the NIC, e.g. PUBLIC.
:type nic_name: ``str``
@@ -1050,72 +1892,94 @@ class ProfitBricksNodeDriver(NodeDriver):
:return: Instance of class ``ProfitBricksNetworkInterface``
:rtype: :class:`ProfitBricksNetworkInterface`
"""
- action = 'createNic'
- body = {'action': action,
- 'request': 'true',
- 'serverId': node.id,
- 'dhcpActive': str(dhcp_active)
- }
- if lan_id:
- body['lanId'] = str(lan_id)
+ if lan_id is not None:
+ lan_id = str(lan_id)
+
else:
- body['lanId'] = str(1)
+ lan_id = str(1)
+
+ action = node.extra['href'] + '/nics'
+ body = {
+ 'properties': {
+ 'lan': lan_id,
+ 'dhcp': dhcp_active
+ }
+ }
- if ip:
- body['ip'] = ip
+ if ips is not None:
+ body['properties']['ips'] = ips
- if nic_name:
- body['nicName'] = nic_name
+ if nic_name is not None:
+ body['properties']['name'] = nic_name
- data = self.connection.request(action=action,
- data=body,
- method='POST').object
- interfaces = self._to_interfaces(data)
- return interfaces[0]
+ response = self.connection.request(
+ action=action,
+ headers={
+ 'Content-Type': 'application/json'
+ },
+ data=body,
+ method='POST',
+ with_full_url=True
+ )
+
+ return self._to_interface(response.object, response.headers)
def ex_update_network_interface(self, network_interface, name=None,
- lan_id=None, ip=None,
+ lan_id=None, ips=None,
dhcp_active=None):
"""
Updates a network interface.
- :param lan_id: The ID for the LAN.
- :type lan_id: : ``int``
+ :param network_interface: The network interface being updated.
+ :type network_interface: :class:`ProfitBricksNetworkInterface`
- :param ip: The IP address for the NIC.
- :type ip: ``str``
+ :param name: The name of the NIC, e.g. PUBLIC.
+ :type name: ``str``
- :param name: The name of the NIC, e.g. PUBLIC.
- :type name: ``str``
+ :param lan_id: The ID for the LAN.
+ :type lan_id: : ``int``
- :param dhcp_active: Set to false to disable.
- :type dhcp_active: ``bool``
+ :param ips: The IP addresses for the NIC as a list.
+ :type ips: ``list``
- :rtype: : ``bool``
- """
- action = 'updateNic'
- body = {'action': action,
- 'request': 'true',
- 'nicId': network_interface.id
- }
+ :param dhcp_active: Set to false to disable.
+ :type dhcp_active: ``bool``
- if name:
- body['nicName'] = name
+ :return: Instance of class ``ProfitBricksNetworkInterface``
+ :rtype: :class:`ProfitBricksNetworkInterface`
+ """
if lan_id:
- body['lanId'] = str(lan_id)
+ lan_id = str(lan_id)
- if ip:
- body['ip'] = ip
+ action = network_interface.href
+ body = {}
+
+ if name is not None:
+ body['name'] = name
+
+ if lan_id is not None:
+ body['lan'] = str(lan_id)
+
+ if ips is not None:
+ body['ips'] = ips
if dhcp_active is not None:
- body['dhcpActive'] = str(dhcp_active).lower()
+ body['dhcp'] = dhcp_active
- self.connection.request(action=action,
- data=body, method='POST').object
+ response = self.connection.request(
+ action=action,
+ headers={
+ 'Content-Type':
+ 'application/json'
+ },
+ data=body,
+ method='PATCH',
+ with_full_url=True
+ )
- return True
+ return self._to_interface(response.object, response.headers)
def ex_destroy_network_interface(self, network_interface):
"""
@@ -1127,378 +1991,1981 @@ class ProfitBricksNodeDriver(NodeDriver):
:rtype: : ``bool``
"""
- action = 'deleteNic'
- body = {'action': action,
- 'nicId': network_interface.id}
+ action = network_interface.href
- self.connection.request(action=action,
- data=body, method='POST').object
+ self.connection.request(
+ action=action,
+ method='DELETE',
+ with_full_url=True
+ )
return True
- def ex_set_inet_access(self, datacenter,
- network_interface, internet_access=True):
+ def ex_set_inet_access(self, network_interface, internet_access=True):
+ """
+ Add/remove public internet access to an interface.
- action = 'setInternetAccess'
+ :param network_interface: The NIC you wish to update.
+ :type network_interface: :class:`ProfitBricksNetworkInterface`
- body = {'action': action,
- 'dataCenterId': datacenter.id,
- 'lanId': network_interface.extra['lan_id'],
- 'internetAccess': str(internet_access).lower()
- }
+ :return: Instance of class ``ProfitBricksNetworkInterface``
+ :rtype: :class:`ProfitBricksNetworkInterface`
+ """
- self.connection.request(action=action,
- data=body, method='POST').object
+ action = network_interface.href
+ body = {
+ 'nat': internet_access
+ }
- return True
+ response = self.connection.request(
+ action=action,
+ headers={
+ 'Content-Type':
+ 'application/json'
+ },
+ data=body,
+ method='PATCH',
+ with_full_url=True
+ )
+
+ return self._to_interface(response.object, response.headers)
"""
- Private Functions
+ Firewall Rule Extension Functions
"""
- def _to_datacenters(self, object):
- return [self._to_datacenter(
- datacenter) for datacenter in object.findall('.//return')]
-
- def _to_datacenter(self, datacenter):
- datacenter_id = datacenter.find('dataCenterId').text
- if ET.iselement(datacenter.find('dataCenterName')):
- datacenter_name = datacenter.find('dataCenterName').text
- else:
- datacenter_name = None
- version = datacenter.find('dataCenterVersion').text
- if ET.iselement(datacenter.find('provisioningState')):
- provisioning_state = datacenter.find('provisioningState').text
- else:
- provisioning_state = None
- if ET.iselement(datacenter.find('location')):
- location = datacenter.find('location').text
- else:
- location = None
-
- provisioning_state = self.PROVISIONING_STATE.get(provisioning_state,
- NodeState.UNKNOWN)
-
- return Datacenter(id=datacenter_id,
- name=datacenter_name,
- version=version,
- driver=self.connection.driver,
- extra={'provisioning_state': provisioning_state,
- 'location': location})
-
- def _to_images(self, object):
- return [self._to_image(image) for image in object.findall('.//return')]
-
- def _to_image(self, image):
- image_id = image.find('imageId').text
- image_name = image.find('imageName').text
- image_size = image.find('imageSize').text
- image_type = image.find('imageType').text
- os_type = image.find('osType').text
- public = image.find('public').text
- writeable = image.find('writeable').text
-
- if ET.iselement(image.find('cpuHotpluggable')):
- cpu_hotpluggable = image.find('cpuHotpluggable').text
- else:
- cpu_hotpluggable = None
+ def ex_list_firewall_rules(self, network_interface):
+ """
+ Fetch firewall rules for a network interface.
- if ET.iselement(image.find('memoryHotpluggable')):
- memory_hotpluggable = image.find('memoryHotpluggable').text
- else:
- memory_hotpluggable = None
+ :param network_interface: The network interface.
+ :type network_interface: :class:`ProfitBricksNetworkInterface`
- if ET.iselement(image.find('location')):
- if image.find('region'):
- image_region = image.find('region').text
+ :return: ``list`` of class ``ProfitBricksFirewallRule``
+ :rtype: `list`
+ """
+ action = network_interface.href + '/firewallrules'
+ response = self.connection.request(
+ action=action,
+ method='GET',
+ params={'depth': 3},
+ with_full_url=True
+ )
+
+ return self._to_firewall_rules(response.object)
+
+ def ex_describe_firewall_rule(
+ self,
+ ex_href=None,
+ ex_datacenter_id=None,
+ ex_server_id=None,
+ ex_nic_id=None,
+ ex_firewall_rule_id=None
+ ):
+ """
+ Fetch data for a firewall rule.
+
+ :param href: The href of the firewall rule you wish to describe.
+ :type href: ``str``
+
+ :param ex_datacenter_id: The ID of parent data center
+ of the NIC you wish to describe.
+ :type ex_datacenter_id: ``str``
+
+ :param ex_server_id: The server the NIC is connected to.
+ :type ex_server_id: ``str``
+
+ :param ex_nic_id: The ID of the NIC.
+ :type ex_nic_id: ``str``
+
+ :param ex_firewall_rule_id: The ID of the firewall rule.
+ :type ex_firewall_rule_id: ``str``
+
+ :return: Instance class ``ProfitBricksFirewallRule``
+ :rtype: :class:`ProfitBricksFirewallRule`
+ """
+
+ use_full_url = True
+
+ if ex_href is None:
+ if (
+ ex_datacenter_id is None or
+ ex_server_id is None or
+ ex_nic_id is None or
+ ex_firewall_rule_id is None
+ ):
+ raise ValueError(
+ (
+ 'IDs are required for the data '
+ 'center, server, network interface',
+ 'and firewall rule.'
+ )
+ )
else:
- image_region = None
- else:
- image_region = None
-
- return NodeImage(id=image_id,
- name=image_name,
- driver=self.connection.driver,
- extra={'image_size': image_size,
- 'image_type': image_type,
- 'cpu_hotpluggable': cpu_hotpluggable,
- 'memory_hotpluggable': memory_hotpluggable,
- 'os_type': os_type,
- 'public': public,
- 'location': image_region,
- 'writeable': writeable})
+ use_full_url = False
+ ex_href = (
+ 'datacenters/{datacenter_id}'
+ '/servers/{server_id}'
+ '/nics/{nic_id}'
+ '/firewallrules/{firewall_rule_id}'
+ ).format(
+ datacenter_id=ex_datacenter_id,
+ server_id=ex_server_id,
+ nic_id=ex_nic_id,
+ firewall_rule_id=ex_firewall_rule_id
+ )
+
+ response = self.connection.request(
+ action=ex_href,
+ method='GET',
+ with_full_url=use_full_url
+ )
+
+ return self._to_firewall_rule(response.object)
+
+ def ex_create_firewall_rule(self, network_interface, protocol,
+ name=None, source_mac=None,
+ source_ip=None, target_ip=None,
+ port_range_start=None, port_range_end=None,
+ icmp_type=None, icmp_code=None):
+ """
+ Create a firewall rule for a network interface.
+
+ :param network_interface: The network interface to
+ attach the firewall rule to.
+ :type: network_interface: :class:`ProfitBricksNetworkInterface`
+
+ :param protocol: The protocol for the rule (TCP, UDP, ICMP, ANY)
+ :type protocol: ``str``
+
+ :param name: The name for the firewall rule
+ :type name: ``str``
+
+ :param source_mac: Only traffic originating from the respective
+ MAC address is allowed.
+ Valid format: aa:bb:cc:dd:ee:ff.
+ Value null allows all source MAC address.
+ :type source_mac: ``str``
+
+ :param source_ip: Only traffic originating from the respective IPv4
+ address is allowed. Value null allows all source IPs.
+ :type source_ip: ``str``
+
+ :param target_ip: In case the target NIC has multiple IP addresses,
+ only traffic directed to the respective IP address
+ of the NIC is allowed.
+ Value null allows all target IPs.
+ :type target_ip: ``str``
+
+ :param port_range_start: Defines the start range of the allowed port
+ (from 1 to 65534) if protocol TCP or UDP is chosen.
+ Leave portRangeStart and portRangeEnd value null
+ to allow all ports.
+ type: port_range_start: ``int``
+
+ :param port_range_end: Defines the end range of the allowed port
+ (from 1 to 65534) if protocol TCP or UDP is chosen.
+ Leave portRangeStart and portRangeEnd value null
+ to allow all ports.
+ type: port_range_end: ``int``
+
+ :param icmp_type: Defines the allowed type (from 0 to 254) if the
+ protocol ICMP is chosen. Value null allows all types.
+ :type icmp_type: ``int``
+
+ :param icmp_code: Defines the allowed code (from 0 to 254) if
+ protocol ICMP is chosen. Value null allows all codes.
+ :type icmp_code: ``int``
+
+ :return: Instance class ``ProfitBricksFirewallRule``
+ :rtype: :class:`ProfitBricksFirewallRule`
+ """
+
+ action = network_interface.href + '/firewallrules'
+ body = {
+ 'properties': {
+ 'protocol': protocol
+ }
+ }
- def _to_nodes(self, object):
- return [self._to_node(n) for n in object.findall('.//return')]
+ if name is not None:
+ body['properties']['name'] = name
- def _to_node(self, node):
- """
- Convert the request into a node Node
+ if source_mac is not None:
+ body['properties']['sourceMac'] = source_mac
+
+ if source_ip is not None:
+ body['properties']['sourceIp'] = source_ip
+
+ if target_ip is not None:
+ body['properties']['targetIp'] = target_ip
+
+ if port_range_start is not None:
+ body['properties']['portRangeStart'] = str(port_range_start)
+
+ if port_range_end is not None:
+ body['properties']['portRangeEnd'] = str(port_range_end)
+
+ if icmp_type is not None:
+ body['properties']['icmpType'] = str(icmp_type)
+
+ if icmp_code is not None:
+ body['properties']['icmpType'] = str(icmp_code)
+
+ response = self.connection.request(
+ action=action,
+ headers={
+ 'Content-Type': 'ap
<TRUNCATED>