Source code for libcloud.compute.drivers.profitbricks

# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""ProfitBricks Compute driver
"""
import base64

import json
import copy
import time
import urllib

from libcloud.utils.py3 import b
from libcloud.compute.providers import Provider
from libcloud.common.base import ConnectionUserAndKey, JsonResponse
from libcloud.compute.base import Node, NodeDriver, NodeLocation, NodeSize
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',
    'API_HOST',
    'ProfitBricksNodeDriver',
    'Datacenter',
    'ProfitBricksNetworkInterface',
    'ProfitBricksFirewallRule',
    'ProfitBricksLan',
    'ProfitBricksLoadBalancer',
    'ProfitBricksAvailabilityZone',
    'ProfitBricksIPBlock'
]

API_HOST = 'api.profitbricks.com'
API_VERSION = '/cloudapi/v3/'


class ProfitBricksResponse(JsonResponse):
    """
    ProfitBricks response parsing.
    """
    def parse_error(self):
        http_code = None
        fault_code = None
        message = None
        try:
            body = json.loads(self.body)
            if 'httpStatus' in body:
                http_code = body['httpStatus']
            else:
                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 = '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): %s,
                Message(s): %s
            '''
            % (http_code, fault_code, message), driver=self)


class ProfitBricksConnection(ConnectionUserAndKey):
    """
    Represents a single connection to the ProfitBricks endpoint.
    """
    host = API_HOST
    api_prefix = API_VERSION
    responseCls = ProfitBricksResponse

    def add_default_headers(self, headers):
        headers['Authorization'] = 'Basic %s' % (base64.b64encode(
            b('%s:%s' % (self.user_id, self.key))).decode('utf-8'))

        return headers

    def encode_data(self, data):
        '''
        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

        elif type(data) is dict:
            return json.dumps(data)

        else:
            return ''

    def request(self, action, params=None, data=None, headers=None,
                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
        )


[docs]class Datacenter(UuidMixin): """ Class which stores information about ProfitBricks datacenter instances. :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 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, href, name, version, driver, extra=None): self.id = str(id) self.href = href self.name = name self.version = version self.driver = driver self.extra = extra or {} UuidMixin.__init__(self) def __repr__(self): return (( '<Datacenter: id=%s, href=%s name=%s, version=%s, driver=%s> ...>') % (self.id, self.href, self.name, self.version, self.driver.name))
[docs]class ProfitBricksNetworkInterface(object): """ Class which stores information about ProfitBricks network interfaces. :param id: The network interface ID. :type id: ``str`` :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))
[docs]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))
[docs]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))
[docs]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, 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 (('ProfitBricksLoadbalancer: id=%s, name=%s, href=%s>') % (self.id, self.name, self.href))
[docs]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. """ def __init__(self, name): self.name = name def __repr__(self): return (('<ProfitBricksAvailabilityZone: name=%s>') % (self.name))
[docs]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) )
[docs]class ProfitBricksNodeDriver(NodeDriver): """ Base ProfitBricks node driver. """ connectionCls = ProfitBricksConnection name = 'ProfitBricks' website = 'http://www.profitbricks.com' type = Provider.PROFIT_BRICKS PROVISIONING_STATE = { 'AVAILABLE': NodeState.RUNNING, 'BUSY': NodeState.PENDING, 'INACTIVE': NodeState.PENDING } NODE_STATE_MAP = { 'NOSTATE': NodeState.UNKNOWN, 'RUNNING': NodeState.RUNNING, 'BLOCKED': NodeState.STOPPED, 'PAUSE': NodeState.PAUSED, 'SHUTDOWN': NodeState.STOPPING, 'SHUTOFF': NodeState.STOPPED, 'CRASHED': NodeState.ERROR, 'AVAILABLE': NodeState.RUNNING, 'BUSY': NodeState.PENDING } AVAILABILITY_ZONE = { '1': {'name': 'AUTO'}, '2': {'name': 'ZONE_1'}, '3': {'name': 'ZONE_2'}, } """ ProfitBricks is unique in that they allow the user to define all aspects of the instance size, i.e. disk size, core size, and memory size. These are instance types that match up with what other providers support. You can configure disk size, core size, and memory size using the ``ex_`` parameters on the create_node method. """ PROFIT_BRICKS_GENERIC_SIZES = { '1': { 'id': '1', 'name': 'Micro', 'ram': 1024, 'disk': 50, 'cores': 1 }, '2': { 'id': '2', 'name': 'Small Instance', 'ram': 2048, 'disk': 50, 'cores': 1 }, '3': { 'id': '3', 'name': 'Medium Instance', 'ram': 4096, 'disk': 50, 'cores': 2 }, '4': { 'id': '4', 'name': 'Large Instance', 'ram': 7168, 'disk': 50, 'cores': 4 }, '5': { 'id': '5', 'name': 'ExtraLarge Instance', 'ram': 14336, 'disk': 50, 'cores': 8 }, '6': { 'id': '6', 'name': 'Memory Intensive Instance Medium', 'ram': 28672, 'disk': 50, 'cores': 4 }, '7': { 'id': '7', 'name': 'Memory Intensive Instance Large', 'ram': 57344, 'disk': 50, 'cores': 8 } } """ Core Functions """
[docs] def list_sizes(self): """ Lists all sizes :return: A list of all configurable node sizes. :rtype: ``list`` of :class:`NodeSize` """ sizes = [] for key, values in self.PROFIT_BRICKS_GENERIC_SIZES.items(): node_size = self._to_node_size(values) sizes.append(node_size) return sizes
[docs] def list_images(self, image_type=None, is_public=True): """ List all images with an optional filter. :param image_type: The image type (HDD, CDROM) :type image_type: ``str`` :param is_public: Image is public :type is_public: ``bool`` :return: ``list`` of :class:`NodeImage` :rtype: ``list`` """ response = self.connection.request( action='images', params={'depth': 1}, method='GET' ) return self._to_images(response.object, image_type, is_public)
[docs] def list_locations(self): """ List all locations. :return: ``list`` of :class:`NodeLocation` :rtype: ``list`` """ return self._to_locations(self.connection.request( action='locations', params={'depth': 1}, method='GET').object )
""" Node functions """
[docs] def list_nodes(self): """ List all nodes. :return: ``list`` of :class:`Node` :rtype: ``list`` """ 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 nodes
[docs] def reboot_node(self, node): """ Reboots the node. :rtype: ``bool`` """ action = node.extra['href'] + '/reboot' self.connection.request( action=action, method='POST', with_full_url=True ) return True
[docs] 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. image is optional as long as you pass ram, cores, and disk to the method. ProfitBricks allows you to adjust compute resources at a much more granular level. :param name: The name for the new node. :param type: ``str`` :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` :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 it in. :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_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_cores: The number of cores required. :type ex_cores: ``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` """ """ 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: ''' 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. ''' new_datacenter = self._create_new_datacenter_for_node( name=name, location=location ) ''' 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: raise ValueError('You need to either pass a ' 'NodeSize or specify ex_ram as ' 'an extra parameter.') if not ex_cores: raise ValueError('You need to either pass a ' 'NodeSize or specify ex_cores as ' 'an extra parameter.') ''' 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 ' 'StorageVolume, a NodeSize, or specify ' 'ex_disk as an extra parameter.') ''' You can override the suggested sizes by passing in unique values for ram, cores, and disk allowing you to size it for your specific use. ''' 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 if not ex_cores: ex_cores = size.extra['cores'] action = ex_datacenter.href + '/servers' body = { 'properties': { 'name': name, 'ram': ex_ram, 'cores': ex_cores }, 'entities': { 'volumes': { 'items': [] } } } ''' If we are using a pre-existing storage volume. ''' 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() ''' 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': [] } } } 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)
[docs] def destroy_node(self, node, ex_remove_attached_disks=False): """ Destroys a node. :param node: The node you wish to destroy. :type volume: :class:`Node` :param ex_remove_attached_disks: True to destroy all attached volumes. :type ex_remove_attached_disks: : ``bool`` :rtype: : ``bool`` """ 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
""" Volume Functions """
[docs] def list_volumes(self): """ List all volumes attached to a data center. :return: ``list`` of :class:`StorageVolume` :rtype: ``list`` """ datacenters = self.ex_list_datacenters() volumes = list() for datacenter in datacenters: volumes_href = datacenter.extra['entities']['volumes']['href'] response = self.connection.request( action=volumes_href, params={'depth': 3}, method='GET', with_full_url=True ) mapped_volumes = self._to_volumes(response.object) volumes += mapped_volumes return volumes
[docs] def attach_volume(self, node, volume): """ Attaches a volume. :param node: The node to which you're attaching the volume. :type node: :class:`Node` :param volume: The volume you're attaching. :type volume: :class:`StorageVolume` :return: Instance of class ``StorageVolume`` :rtype: :class:`StorageVolume` """ action = node.extra['href'] + '/volumes' body = { 'id': volume.id } 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)
[docs] 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 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` :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_ssh_keys: Optional SSH keys. :type ex_ssh_keys: ``dict`` :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` """ 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'] ) ) action = ex_datacenter.href + '/volumes' body = { 'properties': { 'size': size, 'image': image.id } } 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)
[docs] 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`` """ action = node.extra['href'] + '/volumes/{volume_id}'.format( volume_id=volume.id ) self.connection.request( action=action, method='DELETE', with_full_url=True ) return True
[docs] def destroy_volume(self, volume): """ Destroys a volume. :param volume: The volume you're destroying. :type volume: :class:`StorageVolume` :rtype: : ``bool`` """ action = volume.extra['href'] self.connection.request( action=action, method='DELETE', with_full_url=True ) return True
""" Volume snapshot functions """
[docs] def list_snapshots(self): """ Fetches as a list of all snapshots :return: ``list`` of class ``VolumeSnapshot`` :rtype: `list` """ response = self.connection.request( action='/snapshots', params={'depth': 3}, method='GET' ) return self._to_snapshots(response.object)
[docs] def create_volume_snapshot(self, volume): """ Creates a snapshot for a volume :param volume: The volume you're creating a snapshot for. :type volume: :class:`StorageVolume` :return: Instance of class ``VolumeSnapshot`` :rtype: :class:`VolumeSnapshot` """ 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)
[docs] def destroy_volume_snapshot(self, snapshot): """ Delete a snapshot :param snapshot: The snapshot you wish to delete. :type: snapshot: :class:`VolumeSnapshot` :rtype ``bool`` """ action = snapshot.extra['href'] self.connection.request( action=action, method='DELETE', with_full_url=True ) return True
""" Extension Functions """ """ Server Extension Functions """
[docs] 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` :rtype: : ``bool`` """ action = node.extra['href'] + '/stop' self.connection.request( action=action, method='POST', with_full_url=True ) return True
[docs] def ex_start_node(self, node): """ Starts a node. :param node: The node you wish to start. :type node: :class:`Node` :rtype: ``bool`` """ action = node.extra['href'] + '/start' self.connection.request( action=action, method='POST', with_full_url=True ) return True
[docs] def ex_list_availability_zones(self): """ Returns a list of availability zones. :return: ``list`` of :class:`ProfitBricksAvailabilityZone` :rtype: ``list`` """ availability_zones = [] for key, values in self.AVAILABILITY_ZONE.items(): name = copy.deepcopy(values)["name"] availability_zone = ProfitBricksAvailabilityZone( name=name ) availability_zones.append(availability_zone) return availability_zones
[docs] def ex_list_attached_volumes(self, node): """ Returns a list of attached volumes for a server :param node: The node with the attached volumes. :type node: :class:`Node` :return: ``list`` of :class:`StorageVolume` :rtype: ``list`` """ action = node.extra['entities']['volumes']['href'] response = self.connection.request( action=action, params={'depth': 3}, method='GET', with_full_url=True ) return self._to_volumes(response.object)
[docs] def ex_describe_node( self, ex_href=None, ex_datacenter_id=None, ex_node_id=None ): """ Fetches a node directly by href or by a combination of the datacenter ID and the server ID. :param ex_href: The href (url) of the node you wish to describe. :type ex_href: ``str`` :param ex_datacenter_id: The ID for the data center. :type ex_datacenter_id: ``str`` :param ex_node_id: The ID for the node (server). :type ex_node_id: ``str`` :return: Instance of class ``Node`` :rtype: :class:`Node` """ use_full_url = True 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)
[docs] 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. :param node: The node you wish to update. :type node: :class:`Node` :param name: The new name for the node. :type name: ``str`` :param cores: The number of CPUs the node should have. :type cores: : ``int`` :param ram: The amount of ram the node should have. :type ram: : ``int`` :param availability_zone: Update the availability zone. :type availability_zone: :class:`ProfitBricksAvailabilityZone` :param ex_licence_type: Licence type (WINDOWS, LINUX, OTHER). :type ex_licence_type: ``str`` :param ex_boot_volume: Setting the new boot (HDD) volume. :type ex_boot_volume: :class:`StorageVolume` :param ex_boot_cdrom: Setting the new boot (CDROM) volume. :type ex_boot_cdrom: :class:`StorageVolume` :param ex_cpu_family: CPU family (INTEL_XEON, AMD_OPTERON). :type ex_cpu_family: ``str`` :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 """
[docs] 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` """ body = { 'properties': { 'name': name, 'location': location.id } } 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 } } ] response = self.connection.request( action='datacenters', headers={ 'Content-Type': 'application/json' }, data=body, method='POST' ) return self._to_datacenter(response.object, response.headers)
[docs] def ex_destroy_datacenter(self, datacenter): """ Destroys a datacenter. :param datacenter: The DC you're destroying. :type datacenter: :class:`Datacenter` :rtype: : ``bool`` """ action = datacenter.href self.connection.request( action=action, method='DELETE', with_full_url=True ) return True
[docs] def ex_describe_datacenter(self, ex_href=None, ex_datacenter_id=None): """ Fetches the details for a data center. :param ex_href: The href for the data center you are describing. :type ex_href: ``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` """ use_full_url = True 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)
[docs] def ex_list_datacenters(self): """ Lists all datacenters. :return: ``list`` of :class:`DataCenter` :rtype: ``list`` """ response = self.connection.request( action='datacenters', params={'depth': 2}, method='GET' ) return self._to_datacenters(response.object)
[docs] def ex_rename_datacenter(self, datacenter, name): """ Update a datacenter. :param datacenter: The DC you are renaming. :type datacenter: :class:`Datacenter` :param name: The DC name. :type name: : ``str`` :return: Instance of class ``Datacenter`` :rtype: :class:`Datacenter` """ action = datacenter.href body = { 'name': name } response = self.connection.request( action=action, headers={ 'Content-Type': 'application/json' }, data=body, method='PATCH', with_full_url=True ) return self._to_datacenter(response.object, response.headers)
""" Image Extension Functions """
[docs] def ex_describe_image(self, ex_href=None, ex_image_id=None): """ Describe a ProfitBricks image :param ex_href: The href for the image you are describing :type ex_href: ``str`` :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)
[docs] def ex_delete_image(self, image): """ Delete a private image :param image: The private image you are deleting. :type image: :class:`NodeImage` :rtype: : ``bool`` """ self.connection.request( action=image.extra['href'], method='DELETE', with_full_url=True ) return True
[docs] 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 """
[docs] 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 """
[docs] def ex_list_network_interfaces(self): """ Fetch a list of all network interfaces from all data centers. :return: ``list`` of class ``ProfitBricksNetworkInterface`` :rtype: `list` """ nodes = self.list_nodes() nics = list() 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) return nics
[docs] def ex_describe_network_interface( self, ex_href=None, ex_datacenter_id=None, ex_server_id=None, ex_nic_id=None ): """ Fetch information on a network interface. :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` """ 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)
[docs] def ex_create_network_interface(self, node, lan_id=None, ips=None, nic_name=None, dhcp_active=True): """ Creates a network interface. :param lan_id: The ID for the LAN. :type lan_id: : ``int`` :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`` :param dhcp_active: Set to false to disable. :type dhcp_active: ``bool`` :return: Instance of class ``ProfitBricksNetworkInterface`` :rtype: :class:`ProfitBricksNetworkInterface` """ if lan_id is not None: lan_id = str(lan_id) else: lan_id = str(1) action = node.extra['href'] + '/nics' body = { 'properties': { 'lan': lan_id, 'dhcp': dhcp_active } } if ips is not None: body['properties']['ips'] = ips if nic_name is not None: body['properties']['name'] = nic_name 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)
[docs] def ex_update_network_interface(self, network_interface, name=None, lan_id=None, ips=None, dhcp_active=None): """ Updates a network interface. :param network_interface: The network interface being updated. :type network_interface: :class:`ProfitBricksNetworkInterface` :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 ips: The IP addresses for the NIC as a list. :type ips: ``list`` :param dhcp_active: Set to false to disable. :type dhcp_active: ``bool`` :return: Instance of class ``ProfitBricksNetworkInterface`` :rtype: :class:`ProfitBricksNetworkInterface` """ if lan_id: lan_id = str(lan_id) 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['dhcp'] = dhcp_active 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)
[docs] def ex_destroy_network_interface(self, network_interface): """ Destroy a network interface. :param network_interface: The NIC you wish to describe. :type network_interface: :class:`ProfitBricksNetworkInterface` :rtype: : ``bool`` """ action = network_interface.href self.connection.request( action=action, method='DELETE', with_full_url=True ) return True
[docs] def ex_set_inet_access(self, network_interface, internet_access=True): """ Add/remove public internet access to an interface. :param network_interface: The NIC you wish to update. :type network_interface: :class:`ProfitBricksNetworkInterface` :return: Instance of class ``ProfitBricksNetworkInterface`` :rtype: :class:`ProfitBricksNetworkInterface` """ action = network_interface.href body = { 'nat': internet_access } 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)
""" Firewall Rule Extension Functions """
[docs] def ex_list_firewall_rules(self, network_interface): """ Fetch firewall rules for a network interface. :param network_interface: The network interface. :type network_interface: :class:`ProfitBricksNetworkInterface` :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)
[docs] 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: 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)
[docs] 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 } } if name is not None: body['properties']['name'] = name 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': 'application/json' }, data=body, method='POST', with_full_url=True ) return self._to_firewall_rule(response.object, response.headers)
[docs] def ex_update_firewall_rule(self, firewall_rule, 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): """ Update a firewall rule :param firewall_rule: The firewall rule to update :type: firewall_rule: :class:`ProfitBricksFirewallRule` :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 = firewall_rule.href body = {} if name is not None: body['name'] = name if source_mac is not None: body['sourceMac'] = source_mac if source_ip is not None: body['sourceIp'] = source_ip if target_ip is not None: body['targetIp'] = target_ip if port_range_start is not None: body['portRangeStart'] = str(port_range_start) if port_range_end is not None: body['portRangeEnd'] = str(port_range_end) if icmp_type is not None: body['icmpType'] = str(icmp_type) if icmp_code is not None: body['icmpType'] = str(icmp_code) response = self.connection.request( action=action, headers={ 'Content-Type': 'application/json' }, data=body, method='PATCH', with_full_url=True ) return self._to_firewall_rule(response.object, response.headers)
[docs] def ex_delete_firewall_rule(self, firewall_rule): """ Delete a firewall rule :param firewall_rule: The firewall rule to delete. :type: firewall_rule: :class:`ProfitBricksFirewallRule` :rtype ``bool`` """ action = firewall_rule.href self.connection.request( action=action, method='DELETE', with_full_url=True ) return True
""" LAN extension functions """
[docs] def ex_list_lans(self, datacenter=None): """ List local area network on: - a datacenter if one is specified - all datacenters if none specified :param datacenter: The DC you are renaming. :type datacenter: :class:`Datacenter` :return: ``list`` of class ``ProfitBricksLan`` :rtype: `list` """ if datacenter is not None: action = datacenter.extra['entities']['lans']['href'] request = self.connection.request( action=action, params={'depth': 3}, method='GET', with_full_url=True ) lans = self._to_lans(request.object) else: datacenters = self.ex_list_datacenters() lans = [] for datacenter in datacenters: action = datacenter.extra['entities']['lans']['href'] request = self.connection.request( action=action, params={'depth': 3}, method='GET', with_full_url=True ) lans += self._to_lans(request.object) return lans
[docs] def ex_create_lan(self, datacenter, is_public=False, nics=None): """ Create and attach a Lan to a data center. :param datacenter: The DC you are renaming. :type datacenter: :class:`Datacenter` :param is_public: True if the Lan is to have internet access. :type is_public: ``bool`` :param nics: Optional network interfaces to attach to the lan. :param nics: ``list`` of class ``ProfitBricksNetworkInterface`` :return: Instance class ``ProfitBricksLan`` :rtype: :class:`ProfitBricksLan` """ action = datacenter.extra['entities']['lans']['href'] body = { 'properties': { 'name': 'LAN - {datacenter_name}'.format( datacenter_name=datacenter.name ), 'public': is_public } } if nics is not None: body['entities'] = defaultdict(dict) body['entities']['nics']['items'] = [ {'id': nic.id} for nic in nics ] request = self.connection.request( action=action, headers={ 'Content-Type': 'application/json' }, data=body, method='POST', with_full_url=True ) return self._to_lan(request.object, request.headers)
[docs] def ex_describe_lan( self, ex_href=None, ex_datacenter_id=None, ex_lan_id=None ): """ Fetch data on a local area network :param ex_href: The href of the lan you wish to describe. :type ex_href: ``str`` :param ex_datacenter_id: The ID of the parent datacenter for the LAN. :type ex_datacenter_id: ``str`` :param ex_lan_id: The ID of LAN. :type ex_lan_id: ``str`` :return: Instance class ``ProfitBricksLan`` :rtype: :class:`ProfitBricksLan` """ use_full_url = True if ex_href is None: if ex_datacenter_id is None or ex_lan_id is None: raise ValueError( 'IDs for the data center and LAN are required.' ) else: use_full_url = False ex_href = ( 'datacenters/{datacenter_id}/' 'lans/{lan_id}' ).format( datacenter_id=ex_datacenter_id, lan_id=ex_lan_id ) response = self.connection.request( action=ex_href, method='GET', params={'depth': 1}, with_full_url=use_full_url ) return self._to_lan(response.object)
[docs] def ex_update_lan(self, lan, is_public, name=None): """ Update a local area network :param lan: The lan you wish to update. :type: lan: :class:`ProfitBricksLan` :param is_public: Boolean indicating if the lan faces the public internet. :type is_public: ``bool`` :param name: The name of the lan. :type name: ``str`` :return: Instance class ``ProfitBricksLan`` :rtype: :class:`ProfitBricksLan` """ action = lan.href body = { 'public': is_public } if name is not None: body['name'] = name request = self.connection.request( action=action, headers={ 'Content-Type': 'application/json' }, data=body, method='PATCH', with_full_url=True ) return self._to_lan(request.object, request.headers)
[docs] def ex_delete_lan(self, lan): """ Delete a local area network :param lan: The lan you wish to delete. :type: lan: :class:`ProfitBrickLan` :rtype ``bool`` """ action = lan.href self.connection.request( action=action, method='DELETE', with_full_url=True ) return True
""" Volume extension functions """
[docs] def ex_update_volume( self, volume, ex_storage_name=None, size=None, ex_bus_type=None ): """ Updates a volume. :param volume: The volume you're updating. :type volume: :class:`StorageVolume` :param ex_storage_name: The name of the volume. :type ex_storage_name: ``str`` :param size: The desired size. :type size: ``int`` :param ex_bus_type: Volume bus type (VIRTIO, IDE). :type ex_bus_type: ``str`` :return: Instance of class ``StorageVolume`` :rtype: :class:`StorageVolume` """ if not ex_storage_name: ex_storage_name = volume.name if not size: size = str(volume.size) action = volume.extra['href'] body = { 'name': ex_storage_name, 'size': size } if ex_bus_type is not None: body['bus'] = ex_bus_type response = self.connection.request( action=action, headers={ 'Content-Type': 'application/json' }, data=body, method='PATCH', with_full_url=True ) return self._to_volume(response.object, response.headers)
[docs] def ex_describe_volume( self, ex_href=None, ex_datacenter_id=None, ex_volume_id=None ): """ Fetches and returns a volume :param ex_href: The full href (url) of the volume. :type ex_href: ``str`` :param ex_datacenter_id: The ID of the parent datacenter for the volume. :type ex_datacenter_id: ``str`` :param ex_volume_id: The ID of the volume. :type ex_volume_id: ``str`` :return: Instance of class ``StorageVolume`` :rtype: :class:`StorageVolume` """ use_full_url = True if ex_href is None: if ex_datacenter_id is None or ex_volume_id is None: raise ValueError( 'IDs for the data center and volume are required.' ) else: use_full_url = False ex_href = ( 'datacenters/{datacenter_id}/' 'volumes/{volume_id}' ).format( datacenter_id=ex_datacenter_id, volume_id=ex_volume_id ) response = self.connection.request( action=ex_href, method='GET', params={'depth': 3}, with_full_url=use_full_url ) return self._to_volume(response.object)
[docs] def ex_restore_volume_snapshot(self, volume, snapshot): """ Restores a snapshot for a volume :param volume: The volume you're restoring the snapshot to. :type volume: :class:`StorageVolume` :param snapshot: The snapshot you're restoring to the volume. :type snapshot: :class:`ProfitBricksSnapshot` :rtype ``bool`` """ action = volume.extra['href'] + '/restore-snapshot' data = {'snapshotId': snapshot.id} body = urllib.urlencode(data) self.connection.request( action=action, headers={ 'Content-Type': 'application/x-www-form-urlencoded' }, data=body, method='POST', with_full_url=True ) return True
""" Volume snapshot extension functions """
[docs] def ex_describe_snapshot(self, ex_href=None, ex_snapshot_id=None): """ Fetches and returns a volume snapshot :param ex_href: The full href (url) of the snapshot. :type ex_href: ``str`` :param ex_snapshot_id: The ID of the snapshot. :type ex_snapshot_id: ``str`` :return: Instance of class ``ProfitBricksSnapshot`` :rtype: :class:`ProfitBricksSnapshot` """ use_full_url = True if ex_href is None: if ex_snapshot_id is None: raise ValueError( 'The snapshot ID is required.' ) else: use_full_url = False ex_href = ( 'snapshots/{snapshot_id}' ).format( snapshot_id=ex_snapshot_id ) response = self.connection.request( action=ex_href, params={'depth': 3}, method='GET', with_full_url=use_full_url ) return self._to_snapshot(response.object)
[docs] def ex_update_snapshot( self, snapshot, name=None, description=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, licence_type=None ): """ Updates a snapshot :param snapshot: The snapshot you're restoring to the volume. :type snapshot: :class:`VolumeSnapshot` :param name: The snapshot name :type name: `str` :param description: The snapshot description :type description: `str` :param cpu_hot_plug: Snapshot CPU is hot pluggalbe :type cpu_hot_plug: `str` :param cpu_hot_unplug: Snapshot CPU is hot unpluggalbe :type cpu_hot_unplug: `str` :param ram_hot_plug: Snapshot RAM is hot pluggalbe :type ram_hot_plug: `str` :param ram_hot_unplug: Snapshot RAM is hot unpluggalbe :type ram_hot_unplug: `str` :param nic_hot_plug: Snapshot Network Interface is hot pluggalbe :type nic_hot_plug: `str` :param nic_hot_unplug: Snapshot Network Interface is hot unpluggalbe :type nic_hot_unplug: `str` :param disc_virtio_hot_plug: Snapshot VIRTIO disk is hot pluggalbe :type disc_virtio_hot_plug: `str` :param disc_virtio_hot_unplug: Snapshot VIRTIO disk is hot unpluggalbe :type disc_virtio_hot_unplug: `str` :param disc_scsi_hot_plug: Snapshot SCSI disk is hot pluggalbe :type disc_scsi_hot_plug: `str` :param disc_scsi_hot_unplug: Snapshot SCSI disk is hot unpluggalbe :type disc_scsi_hot_unplug: `str` :param licence_type: The snapshot licence_type :type licence_type: `str` :return: Instance of class ``VolumeSnapshot`` :rtype: :class:`VolumeSnapshot` """ action = snapshot.extra['href'] body = {} if name is not None: body['name'] = name if description is not None: body['description'] = description if cpu_hot_plug is not None: body['cpuHotPlug'] = cpu_hot_plug if cpu_hot_unplug is not None: body['cpuHotUnplug'] = cpu_hot_unplug if ram_hot_plug is not None: body['ramHotPlug'] = ram_hot_plug if ram_hot_unplug is not None: body['ramHotUnplug'] = ram_hot_unplug if nic_hot_plug is not None: body['nicHotPlug'] = nic_hot_plug if nic_hot_unplug is not None: body['nicHotUnplug'] = nic_hot_unplug if disc_virtio_hot_plug is not None: body['discVirtioHotPlug'] = disc_virtio_hot_plug if disc_virtio_hot_unplug is not None: body['discVirtioHotUnplug'] = disc_virtio_hot_unplug if disc_scsi_hot_plug is not None: body['discScsiHotPlug'] = disc_scsi_hot_plug if disc_scsi_hot_unplug is not None: body['discScsiHotUnplug'] = disc_scsi_hot_unplug if licence_type is not None: body['licenceType'] = licence_type response = self.connection.request( action=action, params={ 'Content-Type': 'application/json' }, data=body, method='PATCH', with_full_url=True ) return self._to_snapshot(response.object, response.headers)
""" Load balancer extension functions """
[docs] def ex_list_load_balancers(self): """ Fetches as a list of load balancers :return: ``list`` of class ``ProfitBricksLoadBalancer`` :rtype: `list` """ datacenters = self.ex_list_datacenters() load_balancers = list() for datacenter in datacenters: extra = datacenter.extra load_balancers_href = extra['entities']['loadbalancers']['href'] response = self.connection.request( action=load_balancers_href, params={'depth': 3}, method='GET', with_full_url=True ) mapped_load_balancers = self._to_load_balancers(response.object) load_balancers += mapped_load_balancers return load_balancers
[docs] def ex_describe_load_balancer( self, ex_href=None, ex_datacenter_id=None, ex_load_balancer_id=None ): """ Fetches and returns a load balancer :param href: The full href (url) of the load balancer. :type href: ``str`` :param ex_datacenter_id: The ID of the parent data center for the load balancer. :type ex_datacenter_id: ``str`` :param ex_load_balancer_id: The load balancer ID. :type ex_load_balancer_id: ``str`` :return: Instance of class ``ProfitBricksLoadBalancer`` :rtype: :class:`ProfitBricksLoadBalancer` """ use_full_url = True if ex_href is None: if ( ex_datacenter_id is None or ex_load_balancer_id is None ): raise ValueError( ( 'IDs for the data center and ' 'load balancer are required.' ) ) else: use_full_url = False ex_href = ( 'datacenters/{datacenter_id}/' 'loadbalancers/{load_balancer_id}' ).format( datacenter_id=ex_datacenter_id, load_balancer_id=ex_load_balancer_id ) response = self.connection.request( action=ex_href, params={'depth': 3}, method='GET', with_full_url=use_full_url ) return self._to_load_balancer(response.object)
[docs] def ex_create_load_balancer( self, datacenter, name=None, ip=None, dhcp=None, nics=None ): """ Create and attach a load balancer to a data center. :param datacenter: The DC you are renaming. :type datacenter: :class:`Datacenter` :param name: Load balancer name. :type name: ``str`` :param ip: Load balancer IPV4 address. :type ip: ``str`` :param dhcp: If true, the load balancer will reserve an IP address using DHCP. :type dhcp: ``bool`` :param nics: Optional network interfaces taking part in load balancing. :param nics: ``list`` of class ``ProfitBricksNetworkInterface`` :return: Instance class ``ProfitBricksLoadBalancer`` :rtype: :class:`ProfitBricksLoadBalancer` """ action = datacenter.extra['entities']['loadbalancers']['href'] body = { 'properties': { 'name': name or 'Load Balancer - {datacenter_name}' .format(datacenter_name=datacenter.name) } } if ip is not None: body['properties']['ip'] = ip if dhcp is not None: body['properties']['dhcp'] = dhcp if nics is not None: body['entities'] = defaultdict(dict) body['entities']['balancednics']['items'] = [ {'id': nic.id} for nic in nics ] response = self.connection.request( action=action, headers={ 'Content-Type': 'application/json' }, data=body, method='POST', with_full_url=True ) return self._to_load_balancer(response.object, response.headers)
[docs] def ex_update_load_balancer( self, load_balancer, name=None, ip=None, dhcp=None ): """ Update a load balancer :param load_balancer: The load balancer you wish to update. :type: load_balancer: :class:`ProfitBricksLoadBalancer` :param name: The name of the load balancer. :type name: ``str`` :param ip: The IPV4 address of the load balancer. :type ip: ``str`` :param dhcp: If true, the load balancer will reserve an IP address using DHCP. :type dhcp: ``bool`` :return: Instance class ``ProfitBricksLoadBalancer`` :rtype: :class:`ProfitBricksLoadBalancer` """ action = load_balancer.href body = {} if name is not None: body['name'] = name if ip is not None: body['ip'] = ip if dhcp is not None: body['dhcp'] = dhcp response = self.connection.request( action=action, headers={ 'Content-Type': 'application/json' }, data=body, method='PATCH', with_full_url=True ) return self._to_load_balancer(response.object, response.headers)
[docs] def ex_list_load_balanced_nics(self, load_balancer): """ List balanced network interfaces for a load balancer. :param load_balancer: The load balancer you wish to update. :type: load_balancer: :class:`ProfitBricksLoadBalancer` :return: ``list`` of class ``ProfitBricksNetorkInterface`` :rtype: `list` """ action = load_balancer.extra['entities']['balancednics']['href'] response = self.connection.request( action=action, params={'depth': 3}, method='GET', with_full_url=True ) return self._to_interfaces(response.object)
[docs] def ex_describe_load_balanced_nic( self, ex_href=None, ex_datacenter_id=None, ex_server_id=None, ex_nic_id=None ): """ Fetch information on a load balanced network interface. :param ex_href: The href of the load balanced 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` """ return self.ex_describe_network_interface( ex_href=ex_href, ex_datacenter_id=ex_datacenter_id, ex_server_id=ex_server_id, ex_nic_id=ex_nic_id )
[docs] def ex_attach_nic_to_load_balancer( self, load_balancer, network_interface ): """ Attaches a network interface to a load balancer :param load_balancer: The load balancer you wish to attach the network interface to. :type: load_balancer: :class:`ProfitBricksLoadBalancer` :param network_interface: The network interface being attached. :type: network_interface: :class:`ProfitBricksNetworkInterface` :rtype ``bool`` """ action = load_balancer.extra['entities']['balancednics']['href'] body = { 'id': network_interface.id } self.connection.request( action=action, headers={ 'Content-Type': 'application/json' }, data=body, method='POST', with_full_url=True ) return True
[docs] def ex_remove_nic_from_load_balancer( self, load_balancer, network_interface ): """ Removed a network interface from a load balancer :param load_balancer: The load balancer you wish to remove the network interface from. :type: load_balancer: :class:`ProfitBricksLoadBalancer` :param network_interface: The network interface being removed. :type: network_interface: :class:`ProfitBricksNetworkInterface` :rtype ``bool`` """ action = load_balancer.href + '/balancednics/' + network_interface.id self.connection.request( action=action, method='DELETE', with_full_url=True ) return True
[docs] def ex_delete_load_balancer(self, load_balancer): """ Delete a load balancer :param load_balancer: The load balancer you wish to delete. :type: load_balancer: :class:`ProfitBricksLoadBalancer` :rtype ``bool`` """ action = load_balancer.href self.connection.request( action=action, method='DELETE', with_full_url=True ) return True
""" IP Block extension functions """
[docs] def ex_list_ip_blocks(self): """ List all IP blocks :return: ``list`` of class ``ProfitBricksIPBlock`` :rtype: `list` """ response = self.connection.request( action='ipblocks', params={'depth': 3}, method='GET' ) return self._to_ip_blocks(response.object)
[docs] def ex_create_ip_block(self, location, size, name=None): """ Create an IP block :param location: The location of the IP block. :type location: :class:`NodeLocation` :param size: The size of the IP block. :type size: ``int`` :param name: The name of the IP block. :type name: ``str`` :return: Instance class ``ProfitBricksIPBlock`` :rtype: :class:`ProfitBricksIPBlock` """ body = { 'properties': { 'location': location.id, 'size': size } } if name is not None: body['properties']['name'] = name response = self.connection.request( action='ipblocks', headers={ 'Content-Type': 'application/json' }, data=body, method='POST' ) return self._to_ip_block(response.object, response.headers)
[docs] def ex_describe_ip_block(self, ex_href=None, ex_ip_block_id=None): """ Fetch an IP block :param ex_href: The href of the IP block. :type ex_href: ``str`` :param ex_ip_block_id: The ID of the IP block. :type ex_ip_block_id: ``str`` :return: Instance class ``ProfitBricksIPBlock`` :rtype: :class:`ProfitBricksIPBlock` """ use_full_url = True if ex_href is None: if ex_ip_block_id is None: raise ValueError( 'The IP block ID is required.' ) else: use_full_url = False ex_href = ( 'ipblocks/{ip_block_id}' ).format( ip_block_id=ex_ip_block_id ) response = self.connection.request( action=ex_href, params={'depth': 3}, method='GET', with_full_url=use_full_url ) return self._to_ip_block(response.object)
[docs] def ex_delete_ip_block(self, ip_block): """ Delete an IP block :param ip_block: The IP block you wish to delete. :type: ip_block: :class:`ProfitBricksIPBlock` :rtype ``bool`` """ self.connection.request( action=ip_block.href, method='DELETE', with_full_url=True ) return True
""" Private Functions """ def _to_ip_blocks(self, object): return [self._to_ip_block( ip_block) for ip_block in object['items']] def _to_ip_block(self, ip_block, headers=None): nested = { 'metadata': ip_block['metadata'] } extra = {} MAPPED_ATTRS = { 'metadata': { 'createdDate': 'created_date', 'createdBy': 'created_by', 'etag': 'etag', 'lastModifiedDate': 'last_modified_date', 'lastModifiedBy': 'last_modified_by', 'state': 'state' } } for k, v in MAPPED_ATTRS.items(): for original_name, altered_name in v.items(): extra[altered_name] = nested[k][original_name] if headers is not None: if 'location' in headers: extra['status_url'] = headers['location'] state = self.NODE_STATE_MAP.get( ip_block['metadata']['state'], NodeState.UNKNOWN ) # self, id, name, href, location, size, ips, state, driver, extra=None return ProfitBricksIPBlock( id=ip_block['id'], name=ip_block['properties']['name'], href=ip_block['href'], location=ip_block['properties']['location'], size=ip_block['properties']['size'], ips=ip_block['properties']['ips'] or [], state=state, driver=self.connection.driver, extra=extra ) def _to_load_balancers(self, object): return [self._to_load_balancer( load_balancer) for load_balancer in object['items']] def _to_load_balancer(self, load_balancer, headers=None): nested = { 'props': load_balancer['properties'], 'metadata': load_balancer['metadata'] } extra = {} MAPPED_ATTRS = { 'metadata': { 'createdDate': 'created_date', 'createdBy': 'created_by', 'etag': 'etag', 'lastModifiedDate': 'last_modified_date', 'lastModifiedBy': 'last_modified_by', 'state': 'state' }, 'props': { 'name': 'name', 'ip': 'ip', 'dhcp': 'dhcp' } } for k, v in MAPPED_ATTRS.items(): for original_name, altered_name in v.items(): extra[altered_name] = nested[k][original_name] if headers is not None: if 'location' in headers: extra['status_url'] = headers['location'] if 'entities' in load_balancer: extra['entities'] = load_balancer['entities'] state = self.NODE_STATE_MAP.get( load_balancer['metadata']['state'], NodeState.UNKNOWN ) return ProfitBricksLoadBalancer( id=load_balancer['id'], name=load_balancer['properties']['name'], href=load_balancer['href'], state=state, driver=self.connection.driver, extra=extra ) def _to_snapshots(self, object): return [self._to_snapshot( snapshot) for snapshot in object['items']] def _to_snapshot(self, snapshot, headers=None): nested = { 'props': snapshot['properties'], 'metadata': snapshot['metadata'] } extra = {} MAPPED_ATTRS = { 'metadata': { 'createdDate': 'created_date', 'createdBy': 'created_by', 'etag': 'etag', 'lastModifiedDate': 'last_modified_date', 'lastModifiedBy': 'last_modified_by', 'state': 'state' }, 'props': { 'name': 'name', 'description': 'description', 'location': 'location', 'size': 'size', 'cpuHotPlug': 'cpu_hot_plug', 'cpuHotUnplug': 'cpu_hot_unplug', 'ramHotPlug': 'ram_hot_plug', 'ramHotUnplug': 'ram_hot_unplug', 'nicHotPlug': 'nic_hot_plug', 'nicHotUnplug': 'nic_hot_unplug', 'discVirtioHotPlug': 'disc_virtio_hot_plug', 'discVirtioHotUnplug': 'disc_virtio_hot_unplug', 'discScsiHotPlug': 'disc_scsi_hot_plug', 'discScsiHotUnplug': 'disc_scsi_hot_unplug', 'licenceType': 'licence_type' } } for k, v in MAPPED_ATTRS.items(): for original_name, altered_name in v.items(): extra[altered_name] = nested[k][original_name] if headers is not None: if 'location' in headers: extra['status_url'] = headers['location'] state = self.NODE_STATE_MAP.get( snapshot['metadata']['state'], NodeState.UNKNOWN ) extra['href'] = snapshot['href'] return VolumeSnapshot( id=snapshot['id'], driver=self.connection.driver, size=extra['size'], extra=extra, created=extra['created_date'], state=state, name=extra['name'] ) def _to_lans(self, object): return [self._to_lan( lan) for lan in object['items']] def _to_lan(self, lan, headers=None): nested = { 'props': lan['properties'], 'metadata': lan['metadata'] } extra = {} MAPPED_ATTRS = { 'metadata': { 'createdDate': 'created_date', 'createdBy': 'created_by', 'etag': 'etag', 'lastModifiedDate': 'last_modified_date', 'lastModifiedBy': 'last_modified_by', 'state': 'state' }, 'props': { 'name': 'name', 'public': 'is_public' } } for k, v in MAPPED_ATTRS.items(): for original_name, altered_name in v.items(): extra[altered_name] = nested[k][original_name] if 'entities' in lan: extra['entities'] = lan['entities'] if headers is not None: if 'location' in headers: extra['status_url'] = headers['location'] extra['provisioning_state'] = self.PROVISIONING_STATE.get( lan['metadata']['state'], NodeState.UNKNOWN ) state = self.NODE_STATE_MAP.get( lan['metadata']['state'], NodeState.UNKNOWN ) return ProfitBricksLan( id=lan['id'], name=lan['properties']['name'], href=lan['href'], is_public=lan['properties']['public'], state=state, driver=self.connection.driver, extra=extra ) def _to_datacenters(self, object): return [self._to_datacenter( datacenter) for datacenter in object['items']] def _to_datacenter(self, datacenter, headers=None): nested = { 'props': datacenter['properties'], 'metadata': datacenter['metadata'] } if 'entities' in datacenter: nested['entities'] = datacenter['entities'] extra = {} MAPPED_ATTRS = { 'metadata': { 'createdDate': 'created_date', 'createdBy': 'created_by', 'etag': 'etag', 'lastModifiedDate': 'last_modified_date', 'lastModifiedBy': 'last_modified_by', 'state': 'state' }, 'props': { 'description': 'description', 'features': 'features', 'location': 'location', 'name': 'name', 'version': 'version' } } for k, v in MAPPED_ATTRS.items(): for original_name, altered_name in v.items(): extra[altered_name] = nested[k][original_name] if 'entities' in datacenter: extra['entities'] = datacenter['entities'] if headers is not None: if 'location' in headers: extra['status_url'] = headers['location'] extra['provisioning_state'] = self.PROVISIONING_STATE.get( datacenter['metadata']['state'], NodeState.UNKNOWN ) return Datacenter( id=datacenter['id'], href=datacenter['href'], name=datacenter['properties']['name'], version=datacenter['properties']['version'], driver=self.connection.driver, extra=extra ) def _to_images(self, object, image_type=None, is_public=True): if image_type is not None: images = [ image for image in object['items'] if image['properties']['imageType'] == image_type and image['properties']['public'] == is_public ] else: images = [ image for image in object['items'] if image['properties']['public'] == is_public ] return [self._to_image(image) for image in images] def _to_image(self, image, headers=None): nested = { 'props': image['properties'], 'metadata': image['metadata'] } extra = {} MAPPED_ATTRS = { 'metadata': { 'createdDate': 'created_date', 'createdBy': 'created_by', 'etag': 'etag', 'lastModifiedDate': 'last_modified_date', 'lastModifiedBy': 'last_modified_by', 'state': 'state' }, 'props': { 'name': 'name', 'description': 'description', 'location': 'location', 'size': 'size', 'cpuHotPlug': 'cpu_hot_plug', 'cpuHotUnplug': 'cpu_hot_unplug', 'ramHotPlug': 'ram_hot_plug', 'ramHotUnplug': 'ram_hot_unplug', 'nicHotPlug': 'nic_hot_plug', 'nicHotUnplug': 'nic_hot_unplug', 'discVirtioHotPlug': 'disc_virtio_hot_plug', 'discVirtioHotUnplug': 'disc_virtio_hot_unplug', 'discScsiHotPlug': 'disc_scsi_hot_plug', 'discScsiHotUnplug': 'disc_scsi_hot_unplug', 'licenceType': 'licence_type', 'imageType': 'image_type', 'public': 'public' } } for k, v in MAPPED_ATTRS.items(): for original_name, altered_name in v.items(): extra[altered_name] = nested[k][original_name] if headers is not None: if 'location' in headers: extra['status_url'] = headers['location'] """ Put the href inside extra because we cannot assign it to the NodeImage type. """ extra['href'] = image['href'] return NodeImage( id=image['id'], name=image['properties']['name'], driver=self.connection.driver, extra=extra ) def _to_nodes(self, object): return [self._to_node(n) for n in object['items']] def _to_node(self, node, headers=None): """ Convert the request into a node Node """ nested = { 'props': node['properties'], 'metadata': node['metadata'], 'entities': node['entities'] } extra = {} MAPPED_ATTRS = { 'props': { 'name': 'name', 'availabilityZone': 'availability_zone', 'bootCdrom': 'boot_cdrom', 'bootVolume': 'boot_volume', 'cores': 'cores', 'cpuFamily': 'cpu_family', 'ram': 'ram', 'vmState': 'vm_state' }, 'metadata': { 'createdDate': 'created_date', 'createdBy': 'created_by', 'etag': 'etag', 'lastModifiedBy': 'last_modified_by', 'lastModifiedDate': 'last_modified_date', 'state': 'state' } } for k, v in MAPPED_ATTRS.items(): for original_name, altered_name in v.items(): extra[altered_name] = nested[k][original_name] if headers is not None: if 'location' in headers: extra['status_url'] = headers['location'] state = self.NODE_STATE_MAP.get( node['properties']['vmState'], NodeState.UNKNOWN ) extra['entities'] = nested['entities'] extra['href'] = node['href'] public_ips = [] private_ips = [] if 'nics' in nested['entities']: if 'items' in nested['entities']['nics']: for nic in nested['entities']['nics']['items']: if nic['properties']['nat'] is True: public_ips += nic['properties']['ips'] elif nic['properties']['nat'] is False: private_ips += nic['properties']['ips'] return Node( id=node['id'], name=nested['props']['name'], state=state, public_ips=public_ips, private_ips=private_ips, driver=self.connection.driver, extra=extra ) def _to_volumes(self, object): return [self._to_volume( volume) for volume in object['items']] def _to_volume(self, volume, headers=None): nested = { 'props': volume['properties'], 'metadata': volume['metadata'] } extra = {} MAPPED_ATTRS = { 'props': { 'bus': 'bus', 'size': 'size', 'cpuHotPlug': 'cpu_hot_plug', 'cpuHotUnplug': 'cpu_hot_unplug', 'deviceNumber': 'device_number', 'discScsiHotPlug': 'disc_scsi_hot_plug', 'discScsiHotUnplug': 'disc_scsi_hot_unplug', 'discVirtioHotPlug': 'disc_virtio_hot_plug', 'discVirtioHotUnplug': 'disc_virtio_hot_unplug', 'image': 'image', 'imagePassword': 'image_password', 'licenceType': 'licence_type', 'name': 'name', 'nicHotPlug': 'nic_hot_plug', 'nicHotUnplug': 'nic_hot_unplug', 'ramHotPlug': 'ram_hot_plug', 'ramHotUnplug': 'ram_hot_unplug', 'sshKeys': 'ssh_keys', 'type': 'type', 'deviceNumber': 'device_number' }, 'metadata': { 'createdBy': 'created_by', 'createdDate': 'created_date', 'etag': 'etag', 'lastModifiedBy': 'last_modified_by', 'lastModifiedDate': 'last_modified_date', 'state': 'state' } } for k, v in MAPPED_ATTRS.items(): for original_name, altered_name in v.items(): extra[altered_name] = nested[k][original_name] extra['provisioning_state'] = self.PROVISIONING_STATE.get( volume['metadata']['state'], NodeState.UNKNOWN ) extra['href'] = volume['href'] if 'availabilityZone' in volume['properties']: properties = volume['properties'] extra['availability_zone'] = properties['availabilityZone'] if headers is not None: if 'location' in headers: extra['status_url'] = headers['location'] return StorageVolume( id=volume['id'], name=volume['properties']['name'], size=volume['properties']['size'], driver=self.connection.driver, extra=extra ) def _to_interfaces(self, object): return [self._to_interface( interface) for interface in object['items']] def _to_interface(self, interface, headers=None): nested = { 'props': interface['properties'], 'metadata': interface['metadata'] } extra = {} MAPPED_ATTRS = { 'props': { 'dhcp': 'dhcp', 'firewallActive': 'firewall_active', 'ips': 'ips', 'lan': 'lan', 'mac': 'mac', 'name': 'name', 'nat': 'nat' }, 'metadata': { 'createdDate': 'created_date', 'createdBy': 'created_by', 'etag': 'etag', 'lastModifiedBy': 'last_modified_by', 'lastModifiedDate': 'last_modified_date', 'state': 'state' } } for k, v in MAPPED_ATTRS.items(): for original_name, altered_name in v.items(): extra[altered_name] = nested[k][original_name] if 'entities' in interface: extra['entities'] = interface['entities'] state = self.NODE_STATE_MAP.get( interface['metadata']['state'], NodeState.UNKNOWN ) if headers is not None: if 'location' in headers: extra['status_url'] = headers['location'] return ProfitBricksNetworkInterface( id=interface['id'], name=interface['properties']['name'], href=interface['href'], state=state, extra=extra ) def _to_firewall_rules(self, object): return [self._to_firewall_rule( firewall_rule) for firewall_rule in object['items']] def _to_firewall_rule(self, firewallrule, headers=None): nested = { 'props': firewallrule['properties'], 'metadata': firewallrule['metadata'] } extra = {} MAPPED_ATTRS = { 'props': { 'name': 'name', 'protocol': 'protocol', 'sourceMac': 'source_mac', 'sourceIp': 'source_ip', 'targetIp': 'target_ip', 'icmpCode': 'icmp_code', 'icmpType': 'icmp_type', 'portRangeStart': 'port_range_start', 'portRangeEnd': 'port_range_end' }, 'metadata': { 'createdDate': 'created_date', 'createdBy': 'created_by', 'etag': 'etag', 'lastModifiedDate': 'last_modified_date', 'lastModifiedBy': 'last_modified_by', 'state': 'state' } } for k, v in MAPPED_ATTRS.items(): for original_name, altered_name in v.items(): extra[altered_name] = nested[k][original_name] if headers is not None: if 'location' in headers: extra['status_url'] = headers['location'] state = self.NODE_STATE_MAP.get( firewallrule['metadata']['state'], NodeState.UNKNOWN ) return ProfitBricksFirewallRule( id=firewallrule['id'], name=firewallrule['properties']['name'], href=firewallrule['href'], state=state, extra=extra ) def _to_locations(self, object): return [self._to_location(location) for location in object['items']] def _to_location(self, location): return NodeLocation( id=location['id'], name=location['properties']['name'], country=location['id'].split('/')[0], driver=self.connection.driver ) def _to_node_size(self, data): """ Convert the PROFIT_BRICKS_GENERIC_SIZES into NodeSize """ return NodeSize( id=data["id"], name=data["name"], ram=data["ram"], disk=data["disk"], bandwidth=None, price=None, driver=self.connection.driver, extra={'cores': data["cores"]} ) def _wait_for_datacenter_state( self, datacenter, state=NodeState.RUNNING, timeout=300, interval=5 ): """ Private function that waits the datacenter to transition into the specified state. :return: Datacenter object on success. :rtype: :class:`.Datacenter` """ wait_time = 0 attempts = 0 while attempts < 5: attempts += 1 try: datacenter = self.ex_describe_datacenter( ex_datacenter_id=datacenter.id ) break except BaseHTTPError: time.sleep(interval) if datacenter is None: raise Exception( 'Data center was not ready in time to ' 'complete this operation.' ) while (datacenter.extra['provisioning_state'] != state): datacenter = \ self.ex_describe_datacenter(ex_href=datacenter.href) if datacenter.extra['provisioning_state'] == state: break if wait_time >= timeout: raise Exception( 'Datacenter didn\'t transition to %s state ' 'in %s seconds' % (state, timeout) ) wait_time += interval time.sleep(interval) return datacenter def _create_new_datacenter_for_node( self, name, location=None ): """ Creates a Datacenter for a node. """ dc_name = name + '-DC' if location is None: location = self.ex_describe_location( ex_location_id='us/las' ) return self.ex_create_datacenter( name=dc_name, location=location )