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>