Repository: libcloud
Updated Branches:
  refs/heads/trunk ccc53c520 -> 5a9bf7c96


Loadbalancer driver for Softlayer

Closes #500

Signed-off-by: Tomaz Muraus <[email protected]>


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/d232f792
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/d232f792
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/d232f792

Branch: refs/heads/trunk
Commit: d232f7929a133d188f5787f9c8e8d06acdd8bedf
Parents: ccc53c5
Author: Avi Weit <[email protected]>
Authored: Mon Apr 6 06:10:46 2015 +0000
Committer: Tomaz Muraus <[email protected]>
Committed: Thu Apr 16 14:00:13 2015 -0400

----------------------------------------------------------------------
 CHANGES.rst                                     |   8 +
 .../add_list_remove_balancer_members.py         |  27 ++
 .../loadbalancer/softlayer/destroy_balancer.py  |  11 +
 .../softlayer/ex_place_balancer_order.py        |  25 ++
 .../loadbalancer/softlayer/list_balancers.py    |  13 +
 libcloud/loadbalancer/base.py                   |   7 +-
 libcloud/loadbalancer/drivers/softlayer.py      | 445 +++++++++++++++++++
 libcloud/loadbalancer/providers.py              |   2 +
 libcloud/loadbalancer/types.py                  |   1 +
 libcloud/utils/misc.py                          |   6 +
 10 files changed, 543 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/d232f792/CHANGES.rst
----------------------------------------------------------------------
diff --git a/CHANGES.rst b/CHANGES.rst
index ba5da15..8e8ef4e 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -156,6 +156,14 @@ Storage
   (GITHUB-492, LIBCLOUD-635)
   [Tom Fifield]
 
+Loadbalancer
+~~~~~~~~~~~~
+
+- Add a new driver for Softlayer load-balancing service
+  (https://www.softlayer.com/load-balancing).
+  (GITHUB-500, LIBCLOUD-688)
+  [Avi Weit]
+
 DNS
 ~~~
 

http://git-wip-us.apache.org/repos/asf/libcloud/blob/d232f792/docs/examples/loadbalancer/softlayer/add_list_remove_balancer_members.py
----------------------------------------------------------------------
diff --git 
a/docs/examples/loadbalancer/softlayer/add_list_remove_balancer_members.py 
b/docs/examples/loadbalancer/softlayer/add_list_remove_balancer_members.py
new file mode 100644
index 0000000..ad5bd39
--- /dev/null
+++ b/docs/examples/loadbalancer/softlayer/add_list_remove_balancer_members.py
@@ -0,0 +1,27 @@
+from libcloud.loadbalancer.base import DEFAULT_ALGORITHM, Member
+from libcloud.loadbalancer.types import Provider
+from libcloud.loadbalancer.providers import get_driver
+
+USER_NAME = 'your user name'
+SECRET_KEY = 'your secret key'
+
+cls = get_driver(Provider.SOFTLAYER)
+driver = cls(key=USER_NAME, secret=SECRET_KEY)
+
+balancer = driver.list_balancers()[0]
+
+if balancer.port < 0:
+    # no front-end port defined, configure it with such one
+    driver.ex_configure_load_balancer(balancer, port=80, protocol='http',
+                                      algorithm=DEFAULT_ALGORITHM)
+
+member1 = balancer.attach_member(Member(None, '192.168.88.1', 8000))
+member2 = balancer.attach_member(Member(None, '192.168.88.2', 8080))
+
+print(balancer.list_members())
+
+balancer.detach_member(member1)
+print(balancer.list_members())
+
+balancer.detach_member(member2)
+print(balancer.list_members())

http://git-wip-us.apache.org/repos/asf/libcloud/blob/d232f792/docs/examples/loadbalancer/softlayer/destroy_balancer.py
----------------------------------------------------------------------
diff --git a/docs/examples/loadbalancer/softlayer/destroy_balancer.py 
b/docs/examples/loadbalancer/softlayer/destroy_balancer.py
new file mode 100644
index 0000000..4ccc0d8
--- /dev/null
+++ b/docs/examples/loadbalancer/softlayer/destroy_balancer.py
@@ -0,0 +1,11 @@
+from libcloud.loadbalancer.types import Provider
+from libcloud.loadbalancer.providers import get_driver
+
+USER_NAME = 'your user name'
+SECRET_KEY = 'your secret key'
+
+cls = get_driver(Provider.SOFTLAYER)
+driver = cls(key=USER_NAME, secret=SECRET_KEY)
+
+balancer = driver.list_balancers()[0]
+balancer.destroy()

http://git-wip-us.apache.org/repos/asf/libcloud/blob/d232f792/docs/examples/loadbalancer/softlayer/ex_place_balancer_order.py
----------------------------------------------------------------------
diff --git a/docs/examples/loadbalancer/softlayer/ex_place_balancer_order.py 
b/docs/examples/loadbalancer/softlayer/ex_place_balancer_order.py
new file mode 100644
index 0000000..2810ce5
--- /dev/null
+++ b/docs/examples/loadbalancer/softlayer/ex_place_balancer_order.py
@@ -0,0 +1,25 @@
+from libcloud.compute.base import NodeLocation
+
+from libcloud.loadbalancer.types import Provider
+from libcloud.loadbalancer.providers import get_driver
+
+USER_NAME = 'your user name'
+SECRET_KEY = 'your secret key'
+
+cls = get_driver(Provider.SOFTLAYER)
+driver = cls(key=USER_NAME, secret=SECRET_KEY)
+
+# order loadbalancer with a capacity of 50 connections
+CAPACITY = 50
+# create the balancer in Dallas 5 datacenter
+DATACENTER = 'dal05'
+
+# select package to create balancer from
+packages = driver.ex_list_balancer_packages()
+lb_package = [p for p in packages if p.capacity == CAPACITY][0]
+
+driver.ex_place_balancer_order(lb_package,
+                               NodeLocation(DATACENTER, None, None, None))
+
+print 'Successfully submitted oder request, from package' % \
+    lb_package

http://git-wip-us.apache.org/repos/asf/libcloud/blob/d232f792/docs/examples/loadbalancer/softlayer/list_balancers.py
----------------------------------------------------------------------
diff --git a/docs/examples/loadbalancer/softlayer/list_balancers.py 
b/docs/examples/loadbalancer/softlayer/list_balancers.py
new file mode 100644
index 0000000..93872f2
--- /dev/null
+++ b/docs/examples/loadbalancer/softlayer/list_balancers.py
@@ -0,0 +1,13 @@
+from libcloud.loadbalancer.types import Provider
+from libcloud.loadbalancer.providers import get_driver
+
+USER_NAME = 'your user name'
+SECRET_KEY = 'your secret key'
+
+cls = get_driver(Provider.SOFTLAYER)
+driver = cls(key=USER_NAME, secret=SECRET_KEY)
+
+balancers = driver.list_balancers()
+
+for balancer in balancers:
+    print '%s %s' % (balancer, balancer.extra)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/d232f792/libcloud/loadbalancer/base.py
----------------------------------------------------------------------
diff --git a/libcloud/loadbalancer/base.py b/libcloud/loadbalancer/base.py
index 602ea22..3c65e5e 100644
--- a/libcloud/loadbalancer/base.py
+++ b/libcloud/loadbalancer/base.py
@@ -113,8 +113,9 @@ class LoadBalancer(object):
         return self.driver.destroy_balancer(balancer=self)
 
     def __repr__(self):
-        return ('<LoadBalancer: id=%s, name=%s, state=%s>' % (self.id,
-                self.name, self.state))
+        return ('<LoadBalancer: id=%s, name=%s, state=%s, ip=%s, '
+                'port=%s>' % (self.id, self.name, self.state, self.ip,
+                              self.port))
 
 
 class Algorithm(object):
@@ -127,6 +128,8 @@ class Algorithm(object):
     LEAST_CONNECTIONS = 2
     WEIGHTED_ROUND_ROBIN = 3
     WEIGHTED_LEAST_CONNECTIONS = 4
+    SHORTEST_RESPONSE = 5
+    PERSISTENT_IP = 6
 
 DEFAULT_ALGORITHM = Algorithm.ROUND_ROBIN
 

http://git-wip-us.apache.org/repos/asf/libcloud/blob/d232f792/libcloud/loadbalancer/drivers/softlayer.py
----------------------------------------------------------------------
diff --git a/libcloud/loadbalancer/drivers/softlayer.py 
b/libcloud/loadbalancer/drivers/softlayer.py
new file mode 100644
index 0000000..99181b7
--- /dev/null
+++ b/libcloud/loadbalancer/drivers/softlayer.py
@@ -0,0 +1,445 @@
+# 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 withv
+# 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.
+from libcloud.common.types import LibcloudError
+
+__all__ = [
+    'SoftlayerLBDriver'
+]
+
+from libcloud.utils.misc import find, reverse_dict
+from libcloud.loadbalancer.types import State
+from libcloud.loadbalancer.base import Algorithm, Driver, LoadBalancer,\
+    DEFAULT_ALGORITHM, Member
+from libcloud.common.softlayer import SoftLayerConnection
+
+lb_service = 'SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_'\
+    'VirtualIpAddress'
+
+
+class LBPackage(object):
+
+    """
+    Defines a single Softlayer package to be used when placing orders (
+    e.g. via ex_place_balancer_order method).
+
+    :param id: Package id.
+    :type id: ``int``
+
+    :param name: Package name.
+    :type name: ``str``
+
+    :param description: Package short description.
+    :type description: ``str``
+
+    :param price_id: Id of the price for this package.
+    :type price_id: ``int``
+
+    :param capacity: Provides a numerical representation of the capacity given
+                     in the description of this package.
+    :type capacity: ``int``
+
+    """
+
+    def __init__(self, id, name, description, price_id, capacity):
+        self.id = id
+        self.name = name
+        self.description = description
+        self.price_id = price_id
+        self.capacity = capacity
+
+    def __repr__(self):
+        return (
+            '<LBPackage: id=%s, name=%s, description=%s, price_id=%s, '
+            'capacity=%s>' % (self.id, self.name, self.description,
+                              self.price_id, self.capacity))
+
+
+class SoftlayerLBDriver(Driver):
+    name = 'Softlayer Load Balancing'
+    website = 'http://www.softlayer.com/'
+    connectionCls = SoftLayerConnection
+
+    _VALUE_TO_ALGORITHM_MAP = {
+        'ROUND_ROBIN': Algorithm.ROUND_ROBIN,
+        'LEAST_CONNECTIONS': Algorithm.LEAST_CONNECTIONS,
+        'SHORTEST_RESPONSE': Algorithm.SHORTEST_RESPONSE,
+        'PERSISTENT_IP': Algorithm.PERSISTENT_IP
+    }
+
+    _ALGORITHM_TO_VALUE_MAP = reverse_dict(_VALUE_TO_ALGORITHM_MAP)
+
+    def list_balancers(self):
+
+        mask = {
+            'adcLoadBalancers': {
+                'ipAddress': '',
+                'loadBalancerHardware': {
+                    'datacenter': ''
+                },
+                'virtualServers': {
+                    'serviceGroups': {
+                        'routingMethod': '',
+                        'routingType': '',
+                        'services': {
+                            'ipAddress': ''
+                        }
+                    }
+                }
+            }
+        }
+        res = self.connection.request(
+            'SoftLayer_Account', 'getAdcLoadBalancers',
+            object_mask=mask).object
+
+        return [self._to_balancer(lb) for lb in res]
+
+    def get_balancer(self, balancer_id):
+
+        balancers = self.list_balancers()
+        balancer = find(balancers, lambda b: b.id == balancer_id)
+        if not balancer:
+            raise LibcloudError(value='No balancer found for id: %s' %
+                                balancer_id, driver=self)
+        return balancer
+
+    def list_protocols(self):
+        """
+        Return a list of supported protocols.
+
+        :rtype: ``list`` of ``str``
+        """
+        return ['dns', 'ftp', 'http', 'https', 'tcp', 'udp']
+
+    def balancer_list_members(self, balancer):
+
+        lb = self._get_balancer_model(balancer.id)
+        members = []
+        vs = self._locate_service_group(lb, balancer.port)
+        if vs:
+            if vs['serviceGroups']:
+                srvgrp = vs['serviceGroups'][0]
+                members = [self._to_member(srv, balancer) for
+                           srv in srvgrp['services']]
+
+        return members
+
+    def balancer_attach_member(self, balancer, member):
+
+        lb = self._get_balancer_model(balancer.id)
+        vs = self._locate_service_group(lb, balancer.port)
+        if not vs:
+            raise LibcloudError(value='No service_group found for balancer '
+                                'port: %s' % balancer.port, driver=self)
+
+        if vs['serviceGroups']:
+            services = vs['serviceGroups'][0]['services']
+            services.append(self._to_service_template(member.ip,
+                                                      member.port))
+
+        self.connection.request(lb_service, 'editObject', lb, id=balancer.id)
+
+        return [m for m in balancer.list_members() if m.ip == member.ip][0]
+
+    def balancer_detach_member(self, balancer, member):
+
+        svc_lbsrv = 'SoftLayer_Network_Application_Delivery_Controller_'\
+            'LoadBalancer_Service'
+
+        self.connection.request(svc_lbsrv, 'deleteObject', id=member.id)
+        return True
+
+    def destroy_balancer(self, balancer):
+
+        res_billing = self.connection.request(lb_service, 'getBillingItem',
+                                              id=balancer.id).object
+
+        self.connection.request('SoftLayer_Billing_Item', 'cancelService',
+                                id=res_billing['id'])
+        return True
+
+    def ex_list_balancer_packages(self):
+        """
+        Retrieves the available local load balancer packages.
+
+        :rtype: ``list`` of :class:`LBPackage`
+        """
+        mask = {
+            'prices': ''
+        }
+        res = self.connection.request('SoftLayer_Product_Package', 'getItems',
+                                      id=0, object_mask=mask).object
+
+        res_lb_pkgs = [r for r in res if r['description'].find
+                       ('Load Balancer') != -1]
+        res_lb_pkgs = [r for r in res_lb_pkgs if not r['description'].
+                       startswith('Global')]
+
+        return [self._to_lb_package(r) for r in res_lb_pkgs]
+
+    def ex_place_balancer_order(self, package, location):
+        """
+        Places an order for a local loadbalancer in the specified
+        location.
+
+        :param package: The package to create the loadbalancer from.
+        :type package: :class:`LBPackage`
+
+        :param string location: The location (datacenter) to create the
+                                loadbalancer.
+        :type location: :class:`NodeLocation`
+
+        :return: ``True`` if ex_place_balancer_order was successful.
+        :rtype: ``bool``
+        """
+        data = {
+            'complexType': 'SoftLayer_Container_Product_Order_Network_'
+                           'LoadBalancer',
+            'quantity': 1,
+            'packageId': 0,
+            'location': self._get_location(location.id),
+            'prices': [{'id': package.price_id}]
+        }
+
+        self.connection.request('SoftLayer_Product_Order', 'placeOrder',
+                                data)
+        return True
+
+    def ex_configure_load_balancer(self, balancer, port=80,
+                                   protocol='http',
+                                   algorithm=DEFAULT_ALGORITHM,
+                                   ex_allocation=100):
+        """
+        Configure the loadbalancer by adding it with a front-end port (aka
+        a service group in the Softlayer loadbalancer model).
+
+        Softlayer loadbalancer may be defined with multiple service
+        groups (front-end ports) each defined with a unique port number.
+
+        :param balancer: The loadbalancer.
+        :type  balancer: :class:`LoadBalancer`
+
+        :param port: Port of the service group, defaults to 80.
+        :type  port: ``int``
+
+        :param protocol: Loadbalancer protocol, defaults to http.
+        :type  protocol: ``str``
+
+        :param algorithm: Load balancing algorithm, defaults to
+                            Algorithm.ROUND_ROBIN
+        :type  algorithm: :class:`Algorithm`
+
+        :param ex_allocation: The percentage of the total connection
+                              allocations to allocate for this group.
+        :type  ex_allocation: ``int``
+
+        :return: ``True`` if ex_add_service_group was successful.
+        :rtype: ``bool``
+        """
+        _types = self._get_routing_types()
+        _methods = self._get_routing_methods()
+
+        rt = find(_types, lambda t: t['keyname'] == protocol.upper())
+        if not rt:
+            raise LibcloudError(value='Invalid protocol %s' % protocol,
+                                driver=self)
+
+        value = self._algorithm_to_value(algorithm)
+        meth = find(_methods, lambda m: m['keyname'] == value)
+        if not meth:
+            raise LibcloudError(value='Invalid algorithm %s' % algorithm,
+                                driver=self)
+
+        service_group_template = {
+            'port': port,
+            'allocation': ex_allocation,
+            'serviceGroups': [{
+                'routingTypeId': rt['id'],
+                'routingMethodId': meth['id']
+            }]
+        }
+
+        lb = self._get_balancer_model(balancer.id)
+        if len(lb['virtualServers']) > 0:
+            port = lb['virtualServers'][0]['port']
+            raise LibcloudError(value='Loadbalancer already configured with '
+                                'a service group (front-end port)' % port,
+                                driver=self)
+
+        lb['virtualServers'].append(service_group_template)
+        self.connection.request(lb_service, 'editObject', lb, id=balancer.id)
+        return True
+
+    def _get_balancer_model(self, balancer_id):
+        """
+        Retrieve Softlayer loadbalancer model.
+        """
+        lb_mask = {
+            'virtualServers': {
+                'serviceGroups': {
+                    'services': {
+                        'ipAddress': '',
+                        'groupReferences': '',
+                    }
+                }
+            }
+        }
+
+        lb_res = self.connection.request(lb_service, 'getObject',
+                                         object_mask=lb_mask, id=balancer_id).\
+            object
+        return lb_res
+
+    def _locate_service_group(self, lb, port):
+        """
+        Locate service group with given port.
+
+        Return virtualServers (vs) entry whose port matches the
+        supplied parameter port. For a negative port, just return
+        the first vs entry.
+        None is returned if no match found.
+
+        :param lb: Softlayer loadbalancer model.
+        :type lb: ``dict``
+
+        :param port: loadbalancer front-end port.
+        :type port: ``int``
+
+        :return: Matched entry in the virtualServers array of the supplied
+        model.
+        :rtype: ``dict``
+        """
+        vs = None
+        if port < 0:
+            vs = lb['virtualServers'][0] if lb['virtualServers']\
+                else None
+        else:
+            vs = find(lb['virtualServers'], lambda v: v['port'] == port)
+
+        return vs
+
+    def _get_routing_types(self):
+
+        svc_rtype = 'SoftLayer_Network_Application_Delivery_Controller_'\
+            'LoadBalancer_Routing_Type'
+
+        return self.connection.request(svc_rtype, 'getAllObjects').object
+
+    def _get_routing_methods(self):
+
+        svc_rmeth = 'SoftLayer_Network_Application_Delivery_Controller_'\
+            'LoadBalancer_Routing_Method'
+
+        return self.connection.request(svc_rmeth, 'getAllObjects').object
+
+    def _get_location(self, location_id):
+
+        res = self.connection.request('SoftLayer_Location_Datacenter',
+                                      'getDatacenters').object
+
+        dcenter = find(res, lambda d: d['name'] == location_id)
+        if not dcenter:
+            raise LibcloudError(value='Invalid value %s' % location_id,
+                                driver=self)
+        return dcenter['id']
+
+    def _get_ipaddress(self, ip):
+        svc_ipaddress = 'SoftLayer_Network_Subnet_IpAddress'
+
+        return self.connection.request(svc_ipaddress, 'getByIpAddress',
+                                       ip).object
+
+    def _to_lb_package(self, pkg):
+
+        try:
+            price_id = pkg['prices'][0]['id']
+        except:
+            price_id = -1
+
+        capacity = int(pkg.get('capacity', 0))
+        return LBPackage(id=pkg['id'], name=pkg['keyName'],
+                         description=pkg['description'],
+                         price_id=price_id, capacity=capacity)
+
+    def _to_service_template(self, ip, port):
+        """
+        Builds single member entry in Softlayer loadbalancer model
+        """
+        template = {
+            'enabled': 1,  # enable the service
+            'port': port,  # back-end port
+            'ipAddressId': self._get_ipaddress(ip)['id'],
+            'healthChecks': [{
+                'healthCheckTypeId': 21  # default health check
+            }],
+            'groupReferences': [{
+                'weight': 1
+            }]
+        }
+
+        return template
+
+    def _to_balancer(self, lb):
+        ipaddress = lb['ipAddress']['ipAddress']
+
+        extra = {}
+        extra['connection_limit'] = lb['connectionLimit']
+        extra['ssl_active'] = lb['sslActiveFlag']
+        extra['ssl_enabled'] = lb['sslEnabledFlag']
+        extra['ha'] = lb['highAvailabilityFlag']
+        extra['datacenter'] = \
+            lb['loadBalancerHardware'][0]['datacenter']['name']
+
+        # In Softlayer, there could be multiple group of members (aka service
+        # groups), so retrieve the first one
+        vs = self._locate_service_group(lb, -1)
+        if vs:
+            port = vs['port']
+            if vs['serviceGroups']:
+                srvgrp = vs['serviceGroups'][0]
+                routing_method = srvgrp['routingMethod']['keyname']
+                routing_type = srvgrp['routingType']['keyname']
+                try:
+                    extra['algorithm'] = self.\
+                        _value_to_algorithm(routing_method)
+                except:
+                    pass
+                extra['protocol'] = routing_type.lower()
+
+        if not vs:
+            port = -1
+
+        balancer = LoadBalancer(
+            id=lb['id'],
+            name='',
+            state=State.UNKNOWN,
+            ip=ipaddress,
+            port=port,
+            driver=self.connection.driver,
+            extra=extra
+        )
+
+        return balancer
+
+    def _to_member(self, srv, balancer=None):
+
+        svc_id = srv['id']
+        ip = srv['ipAddress']['ipAddress']
+        port = srv['port']
+
+        extra = {}
+        extra['status'] = srv['status']
+        extra['enabled'] = srv['enabled']
+        return Member(id=svc_id, ip=ip, port=port, balancer=balancer,
+                      extra=extra)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/d232f792/libcloud/loadbalancer/providers.py
----------------------------------------------------------------------
diff --git a/libcloud/loadbalancer/providers.py 
b/libcloud/loadbalancer/providers.py
index f11e450..5a65402 100644
--- a/libcloud/loadbalancer/providers.py
+++ b/libcloud/loadbalancer/providers.py
@@ -38,6 +38,8 @@ DRIVERS = {
     ('libcloud.loadbalancer.drivers.cloudstack', 'CloudStackLBDriver'),
     Provider.GCE:
     ('libcloud.loadbalancer.drivers.gce', 'GCELBDriver'),
+    Provider.SOFTLAYER:
+    ('libcloud.loadbalancer.drivers.softlayer', 'SoftlayerLBDriver'),
 
     # Deprecated
     Provider.RACKSPACE_US:

http://git-wip-us.apache.org/repos/asf/libcloud/blob/d232f792/libcloud/loadbalancer/types.py
----------------------------------------------------------------------
diff --git a/libcloud/loadbalancer/types.py b/libcloud/loadbalancer/types.py
index 6be53e5..8ae82e9 100644
--- a/libcloud/loadbalancer/types.py
+++ b/libcloud/loadbalancer/types.py
@@ -39,6 +39,7 @@ class Provider(object):
     ELB = 'elb'
     CLOUDSTACK = 'cloudstack'
     GCE = 'gce'
+    SOFTLAYER = 'softlayer'
 
     # Deprecated
     RACKSPACE_US = 'rackspace_us'

http://git-wip-us.apache.org/repos/asf/libcloud/blob/d232f792/libcloud/utils/misc.py
----------------------------------------------------------------------
diff --git a/libcloud/utils/misc.py b/libcloud/utils/misc.py
index 3e2eeac..49c1a0c 100644
--- a/libcloud/utils/misc.py
+++ b/libcloud/utils/misc.py
@@ -19,6 +19,7 @@ import binascii
 
 
 __all__ = [
+    'find',
     'get_driver',
     'set_driver',
     'merge_valid_keys',
@@ -33,6 +34,11 @@ __all__ = [
 ]
 
 
+def find(l, predicate):
+    results = [x for x in l if predicate(x)]
+    return results[0] if len(results) > 0 else None
+
+
 def get_driver(drivers, provider):
     """
     Get a driver.

Reply via email to