=== modified file 'libcloud/compute/drivers/opennebula.py'
--- libcloud/compute/drivers/opennebula.py	2011-11-12 17:47:27 +0000
+++ libcloud/compute/drivers/opennebula.py	2011-11-13 04:03:48 +0000
@@ -15,6 +15,7 @@
 # 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.
+
 """
 OpenNebula Driver
 """
@@ -74,6 +75,9 @@
 
 
 class OpenNebulaNodeSize(NodeSize):
+    """
+    NodeSize class for the OpenNebula driver.
+    """
 
     def __init__(self, id, name, ram, disk, bandwidth, price, driver,
                  cpu=None, vcpu=None):
@@ -86,31 +90,87 @@
 
     def __repr__(self):
         return (('<NodeSize: id=%s, name=%s, ram=%s, disk=%s, bandwidth=%s, '
-                 'price=%s, driver=%s, cpu=%s ...>')
+                 'price=%s, driver=%s, cpu=%s, vcpu=%s ...>')
                 % (self.id, self.name, self.ram, self.disk, self.bandwidth,
-                   self.price, self.driver.name, self.cpu))
+                   self.price, self.driver.name, self.cpu, self.vcpu))
 
 
 class OpenNebulaNetwork(object):
     """
-    A virtual network.
-
-    NodeNetwork objects are analogous to physical switches connecting 2
-    or more physical nodes together.
-
-    Apart from name and id, there is no further standard information;
-    other parameters are stored in a driver specific "extra" variable
+    Provide a common interface for handling networks of all types.
+
+    Network objects are analogous to physical switches connecting two or
+    more physical nodes together. The Network object provides the interface in
+    libcloud through which we can manipulate networks in different cloud
+    providers in the same way. Network objects don't actually do much directly
+    themselves, instead the network driver handles the connection to the
+    network.
+
+    You don't normally create a network object yourself; instead you use
+    a driver and then have that create the network for you.
+
+    >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+    >>> driver = DummyNetworkDriver()
+    >>> network = driver.create_network()
+    >>> network = driver.list_networks()[0]
+    >>> network.name
+    'dummy-1'
     """
 
-    def __init__(self, id, name, driver, extra=None):
+    def __init__(self, id, name, address, size, driver, extra=None):
         self.id = str(id)
         self.name = name
+        self.address = address
+        self.size = size
         self.driver = driver
+        self.uuid = self.get_uuid()
         self.extra = extra or {}
 
+    def get_uuid(self):
+        """
+        Unique hash for this network.
+
+        @return: C{string}
+
+        The hash is a function of an SHA1 hash of the network's ID and
+        its driver which means that it should be unique between all
+        networks. In some subclasses (e.g. GoGrid) there is no ID
+        available so the public IP address is used. This means that,
+        unlike a properly done system UUID, the same UUID may mean a
+        different system install at a different time
+
+        >>> from libcloud.network.drivers.dummy import DummyNetworkDriver
+        >>> driver = DummyNetworkDriver()
+        >>> network = driver.create_network()
+        >>> network.get_uuid()
+        'd3748461511d8b9b0e0bfa0d4d3383a619a2bb9f'
+
+        Note, for example, that this example will always produce the
+        same UUID!
+        """
+        return hashlib.sha1("%s:%d" % (self.id, self.driver.type)).hexdigest()
+
+    def destroy(self):
+        """
+        Destroy this network.
+
+        @return: C{bool}
+
+        This calls the networks's driver and destroys the network.
+
+        >>> from libcloud.network.drivers.dummy import DummyNetworkDriver
+        >>> driver = DummyNetworkDriver()
+        >>> network = driver.create_network()
+        >>> network.destroy()
+        True
+        """
+        return self.driver.destroy_network(self)
+
     def __repr__(self):
-        return (('<OpenNebulaNetwork: id=%s, name=%s, driver=%s ...>')
-                % (self.id, self.name, self.driver.name))
+        return (('<OpenNebulaNetwork: uuid=%s, name=%s, address=%s, size=%s, '
+                 'provider=%s ...>')
+                % (self.uuid, self.name, self.address, self.size,
+                   self.driver.name))
 
 
 class OpenNebulaNodeDriver(NodeDriver):
@@ -133,55 +193,72 @@
         'SUSPENDED': NodeState.PENDING,
         'FAILED': NodeState.TERMINATED,
         'UNKNOWN': NodeState.UNKNOWN,
-        'DONE': NodeState.TERMINATED
-    }
+        'DONE': NodeState.TERMINATED}
 
     def __new__(cls, key, secret=None, api_version=DEFAULT_API_VERSION,
                 **kwargs):
         if cls is OpenNebulaNodeDriver:
             if api_version == '1.4':
                 cls = OpenNebula_1_4_NodeDriver
-            elif api_version == '3.0':
-                cls = OpenNebula_3_0_NodeDriver
+            elif api_version == '2.0' or api_version == '2.2' or \
+                 api_version == '3.0':
+                cls = OpenNebula_2_0_NodeDriver
             else:
                 raise NotImplementedError(
                     "No OpenNebulaNodeDriver found for API version %s" %
                     (api_version))
             return super(OpenNebulaNodeDriver, cls).__new__(cls)
 
-    def list_sizes(self, location=None):
-        return [
-            OpenNebulaNodeSize(id=1,
-                name='small',
-                ram=None,
-                disk=None,
-                bandwidth=None,
-                price=None,
-                driver=self),
-            OpenNebulaNodeSize(id=2,
-                name='medium',
-                ram=None,
-                disk=None,
-                bandwidth=None,
-                price=None,
-                driver=self),
-            OpenNebulaNodeSize(id=3,
-                name='large',
-                ram=None,
-                disk=None,
-                bandwidth=None,
-                price=None,
-                driver=self),
-        ]
-
-    def list_nodes(self):
-        return self._to_nodes(self.connection.request('/compute').object)
-
-    def list_images(self, location=None):
-        return self._to_images(self.connection.request('/storage').object)
-
-    def list_locations(self):
-        return [NodeLocation(0,  'OpenNebula', 'ONE', self)]
+    def create_node(self, **kwargs):
+        """
+        Create a new OpenNebula node.
+
+        See L{NodeDriver.create_node} for more keyword args.
+        @keyword networks: List of virtual networks to which this node should
+                           connect. (optional)
+        @type    networks: L{OpenNebulaNetwork} or C{list}
+                           of L{OpenNebulaNetwork}s.
+
+        @keyword context: Custom (key, value) pairs to be injected into
+                          compute node XML description. (optional)
+        @type    context: C{dict}
+        """
+        compute = ET.Element('COMPUTE')
+
+        name = ET.SubElement(compute, 'NAME')
+        name.text = kwargs['name']
+
+        if 'networks' in kwargs:
+            if isinstance(kwargs['networks'], list):
+                for network in kwargs['networks']:
+                    networkGroup = ET.SubElement(compute, 'NETWORK')
+                    if network.address:
+                        nic = ET.SubElement(networkGroup, 'NIC', {'network': '%s' %
+                            (str(network.id)), 'ip': network.address})
+                    else:
+                        nic = ET.SubElement(networkGroup, 'NIC', {'network': '%s' %
+                            (str(network.id))})
+            else:
+                network = kwargs['networks']
+                networkGroup = ET.SubElement(compute, 'NETWORK')
+                if networks.address:
+                    nic = ET.SubElement(networkGroup, 'NIC', {'network': '%s' %
+                        (str(networks.id)), 'ip': networks.address})
+                else:
+                    nic = ET.SubElement(networkGroup, 'NIC', {'network': '%s' %
+                        (str(networks.id))})
+
+        xml = ET.tostring(compute)
+        node = self.connection.request('/compute', method='POST',
+                                       data=xml).object
+
+        return self._to_node(node)
+
+    def destroy_node(self, node):
+        url = '/compute/%s' % (str(node.id))
+        resp = self.connection.request(url, method='DELETE')
+
+        return resp.status == 204
 
     def reboot_node(self, node):
         compute_id = str(node.id)
@@ -203,31 +280,44 @@
 
         return True
 
-    def destroy_node(self, node):
-        url = '/compute/%s' % (str(node.id))
-        resp = self.connection.request(url, method='DELETE')
-
-        return resp.status == 204
-
-    def create_node(self, **kwargs):
-        """Create a new OpenNebula node
-
-        See L{NodeDriver.create_node} for more keyword args.
-        """
-        compute = ET.Element('COMPUTE')
-
-        name = ET.SubElement(compute, 'NAME')
-        name.text = kwargs['name']
-
-        xml = ET.tostring(compute)
-        node = self.connection.request('/compute', method='POST',
-                                       data=xml).object
-
-        return self._to_node(node)
+    def list_nodes(self):
+        return self._to_nodes(self.connection.request('/compute').object)
+
+    def list_images(self, location=None):
+        return self._to_images(self.connection.request('/storage').object)
+
+    def list_sizes(self, location=None):
+        return [
+            OpenNebulaNodeSize(id=1,
+                name='small',
+                ram=None,
+                disk=None,
+                bandwidth=None,
+                price=None,
+                driver=self),
+            OpenNebulaNodeSize(id=2,
+                name='medium',
+                ram=None,
+                disk=None,
+                bandwidth=None,
+                price=None,
+                driver=self),
+            OpenNebulaNodeSize(id=3,
+                name='large',
+                ram=None,
+                disk=None,
+                bandwidth=None,
+                price=None,
+                driver=self),
+        ]
+
+    def list_locations(self):
+        return [NodeLocation(0,  'OpenNebula', 'ONE', self)]
 
     def ex_list_networks(self, location=None):
         """
         List virtual networks on a provider
+
         @return: C{list} of L{OpenNebulaNetwork} objects
         """
         return self._to_networks(self.connection.request('/network').object)
@@ -262,9 +352,9 @@
     def _to_network(self, element):
         return OpenNebulaNetwork(id=element.findtext('ID'),
                       name=element.findtext('NAME'),
-                      driver=self.connection.driver,
-                      extra={'address': element.findtext('ADDRESS'),
-                             'size': element.findtext('SIZE')})
+                      address=None,
+                      size=None,
+                      driver=self.connection.driver)
 
     def _to_nodes(self, object):
         computes = []
@@ -284,8 +374,10 @@
             networks.append(
                 OpenNebulaNetwork(id=element.attrib.get('network', None),
                     name=None,
+                    address=element.attrib.get('ip', None),
+                    size=1,
                     driver=self.connection.driver,
-                    extra={'ip': element.attrib.get('ip', None)}))
+                    extra={}))
 
         return networks
 
@@ -318,14 +410,31 @@
 
 
 class OpenNebula_1_4_NodeDriver(OpenNebulaNodeDriver):
+    """
+    OpenNebula node driver for OpenNebula v1.4.
+    """
+
     pass
 
 
-class OpenNebula_3_0_NodeDriver(OpenNebulaNodeDriver):
+class OpenNebula_2_0_NodeDriver(OpenNebulaNodeDriver):
+    """
+    OpenNebula node driver for OpenNebula v2.0 and greater.
+    """
+
     def create_node(self, **kwargs):
-        """Create a new OpenNebula node
+        """
+        Create a new OpenNebula node.
 
         See L{NodeDriver.create_node} for more keyword args.
+        @keyword networks: List of virtual networks to which this node should
+                           connect. (optional)
+        @type    networks: L{OpenNebulaNetwork} or C{list}
+                           of L{OpenNebulaNetwork}s.
+
+        @keyword context: Custom (key, value) pairs to be injected into
+                          compute node XML description. (optional)
+        @type    context: C{dict}
         """
         compute = ET.Element('COMPUTE')
 
@@ -339,6 +448,24 @@
         storage = ET.SubElement(disk, 'STORAGE', {'href': '/storage/%s' %
                                                   (str(kwargs['image'].id))})
 
+        if 'networks' in kwargs:
+            if isinstance(kwargs['networks'], list):
+                for network in kwargs['networks']:
+                    nic = ET.SubElement(compute, 'NIC')
+                    networkLine = ET.SubElement(nic, 'NETWORK',
+                                {'href': '/network/%s' % (str(network.id))})
+                    if network.address:
+                        ip = ET.SubElement(nic, 'IP')
+                        ip.text = network.address
+            else:
+                network = kwargs['networks']
+                nic = ET.SubElement(compute, 'NIC')
+                networkLine = ET.SubElement(nic, 'NETWORK',
+                                {'href': '/network/%s' % (str(network.id))})
+                if network.address:
+                    ip = ET.SubElement(nic, 'IP')
+                    ip.text = network.address
+
         xml = ET.tostring(compute)
         node = self.connection.request('/compute', method='POST',
                                        data=xml).object
@@ -381,13 +508,6 @@
                    driver=self),
         ]
 
-    def ex_list_networks(self, location=None):
-        """
-        List virtual networks on a provider
-        @return: C{list} of L{OpenNebulaNetwork} objects
-        """
-        return self._to_networks(self.connection.request('/network').object)
-
     def _to_images(self, object):
         images = []
         for element in object.findall('STORAGE'):
@@ -403,7 +523,7 @@
                          name=image.findtext('NAME'),
                          driver=self.connection.driver,
                          extra={'description': image.findtext('DESCRIPTION'),
-                                'TYPE': image.findtext('TYPE'),
+                                'type': image.findtext('TYPE'),
                                 'size': image.findtext('SIZE'),
                                 'fstype': image.findtext('FSTYPE', None)})
 
@@ -414,16 +534,12 @@
             network = element.find('NETWORK')
             network_id = network.attrib['href'].partition('/network/')[2]
 
-            ips = []
-            for ip in element.findall('IP'):
-                ips.append(ip)
-
             networks.append(
                 OpenNebulaNetwork(id=network_id,
-                         name=network.attrib['name'],
+                         name=network.attrib.get('name', None),
+                         address=element.findtext('IP'),
+                         size=1,
                          driver=self.connection.driver,
-                         extra={'ip': ips,
-                                'mac': element.findtext('MAC'),
-                         }))
+                         extra={'mac': element.findtext('MAC')}))
 
         return networks

=== added directory 'libcloud/networking'
=== added file 'libcloud/networking/__init__.py'
--- libcloud/networking/__init__.py	1970-01-01 00:00:00 +0000
+++ libcloud/networking/__init__.py	2011-11-07 08:11:49 +0000
@@ -0,0 +1,3 @@
+"""
+Module for working with virtual networks.
+"""

=== added file 'libcloud/networking/base.py'
--- libcloud/networking/base.py	1970-01-01 00:00:00 +0000
+++ libcloud/networking/base.py	2011-11-13 06:00:23 +0000
@@ -0,0 +1,169 @@
+# 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.
+
+"""
+Provides base classes for working with virtual networks.
+"""
+
+import hashlib
+
+from libcloud.common.base import ConnectionUserAndKey, BaseDriver
+
+
+__all__ = ['Network',
+           'NetworkDriver']
+
+
+class Network(object):
+    """
+    Provide a common interface for handling networks of all types.
+
+    Network objects are analogous to physical switches connecting two or
+    more physical nodes together. The Network object provides the interface in
+    libcloud through which we can manipulate networks in different cloud
+    providers in the same way. Network objects don't actually do much directly
+    themselves, instead the network driver handles the connection to the
+    network.
+
+    You don't normally create a network object yourself; instead you use
+    a driver and then have that create the network for you.
+
+    >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+    >>> driver = DummyNetworkDriver()
+    >>> network = driver.create_network()
+    >>> network = driver.list_networks()[0]
+    >>> network.name
+    'dummy-1'
+    """
+
+    def __init__(self, id, name, address, size, driver, extra=None):
+        self.id = str(id)
+        self.name = name
+        self.address = address
+        self.size = size
+        self.driver = driver
+        self.uuid = self.get_uuid()
+        self.extra = extra or {}
+
+    def get_uuid(self):
+        """
+        Unique hash for this network.
+
+        @return: C{string}
+
+        The hash is a function of an SHA1 hash of the network's ID and
+        its driver which means that it should be unique between all
+        networks. In some subclasses (e.g. GoGrid) there is no ID
+        available so the public IP address is used. This means that,
+        unlike a properly done system UUID, the same UUID may mean a
+        different system install at a different time
+
+        >>> from libcloud.network.drivers.dummy import DummyNetworkDriver
+        >>> driver = DummyNetworkDriver()
+        >>> network = driver.create_network()
+        >>> network.get_uuid()
+        'd3748461511d8b9b0e0bfa0d4d3383a619a2bb9f'
+
+        Note, for example, that this example will always produce the
+        same UUID!
+        """
+        return hashlib.sha1("%s:%d" % (self.id, self.driver.type)).hexdigest()
+
+    def destroy(self):
+        """
+        Destroy this network.
+
+        @return: C{bool}
+
+        This calls the networks's driver and destroys the network.
+
+        >>> from libcloud.network.drivers.dummy import DummyNetworkDriver
+        >>> driver = DummyNetworkDriver()
+        >>> network = driver.create_network()
+        >>> network.destroy()
+        True
+        """
+        return self.driver.destroy_network(self)
+
+    def __repr__(self):
+        return (('<Network: uuid=%s, name=%s, address=%s, size=%s, '
+                 'provider=%s ...>')
+                % (self.uuid, self.name, self.address, self.size,
+                   self.driver.name))
+
+
+class NetworkDriver(BaseDriver):
+    """
+    A base NetworkDriver to derive from.
+    """
+
+    connectionCls = ConnectionUserAndKey
+    name = None
+    type = None
+    port = None
+    features = {"create_network": []}
+
+    def __init__(self, key, secret=None, secure=True, host=None, port=None,
+                 api_version=None):
+        super(NetworkDriver, self).__init__(key=key, secret=secret,
+                                     secure=secure, host=host, port=port,
+                                     api_version=api_version)
+
+    def list_networks(self, location=None):
+        """
+        List virtual networks on a provider.
+
+        @return: C{list} of L{Network} objects.
+        """
+        raise NotImplementedError(
+            'list_networks not implemented for this driver.')
+
+    def destroy_network(self, network):
+        """
+        Destroy a network.
+
+        Depending upon the provider, this may destroy all data associated with
+        the network.
+
+        @return: C{bool} True if the destroy was successful, otherwise False.
+        """
+        raise NotImplementedError(
+            'destroy_network not implemented for this driver.')
+
+    def create_network(self, **kwargs):
+        """
+        Create a new network instance.
+
+        @keyword    name:   String with a name for this new network (required)
+        @type       name:   str
+
+        @keyword    address: IP address based on the Classful Address scheme.
+        @type       address: str
+
+        @keyword    size:   Size of the network in number of IPs within the
+                            network. Size of 255 corresponds to
+                            [address]/24 using the Classless Inter-
+                            Domain Routing scheme.
+        @type       size:   int
+
+        @return: The newly created L{Network}.
+        """
+        raise NotImplementedError(
+            'create_network not implemented for this driver.')
+
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()

=== added directory 'libcloud/networking/drivers'
=== added file 'libcloud/networking/drivers/__init__.py'
--- libcloud/networking/drivers/__init__.py	1970-01-01 00:00:00 +0000
+++ libcloud/networking/drivers/__init__.py	2011-11-07 08:11:49 +0000
@@ -0,0 +1,22 @@
+# 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.
+
+"""
+Drivers for working with different providers.
+"""
+
+
+__all__ = ['dummy',
+           'opennebula']

=== added file 'libcloud/networking/drivers/dummy.py'
--- libcloud/networking/drivers/dummy.py	1970-01-01 00:00:00 +0000
+++ libcloud/networking/drivers/dummy.py	2011-11-13 05:33:38 +0000
@@ -0,0 +1,124 @@
+# 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.
+
+"""
+Dummy Driver
+"""
+
+import uuid
+
+from libcloud.networking.base import NetworkDriver, Network
+from libcloud.networking.types import Provider
+
+
+class DummyNetworkDriver(NetworkDriver):
+    """
+    Dummy network driver.
+
+    This is a fake driver which appears to always create or destroy
+    networks successfully.
+
+    >>> from libcloud.networking.drivers.dummy import DummyNetworkDriver
+    >>> driver = DummyNetworkDriver()
+    >>> network = driver.create_network()
+    >>> network = driver.list_networks()[0]
+    >>> network.name
+    'dummy-1'
+    """
+
+    name = 'Dummy Network Provider'
+    type = Provider.DUMMY
+
+    def __init__(self):
+        self.nl = []
+
+    def get_uuid(self):
+        return str(uuid.uuid4())
+
+    def list_networks(self):
+        """
+        List the networks known to a particular driver.
+
+        >>> from libcloud.networking.drivers.dummy import DummyNetworkDriver
+        >>> driver = DummyNetworkDriver()
+        >>> network_list = driver.list_networks()
+        >>> print network_list
+        []
+
+        Each item in the list returned is a network object from which you
+        can carry out any network actions you wish.
+
+        As more networks are added, list_networks will return them.
+
+        >>> network = driver.create_network()
+        >>> sorted([network.name for network in driver.list_networks()])
+        ['dummy-1']
+        """
+        return self.nl
+
+    def destroy_network(self, network):
+        """
+        Remove the network from the network list.
+
+        >>> from libcloud.networking.drivers.dummy import DummyNetworkDriver
+        >>> driver = DummyNetworkDriver()
+        >>> network = driver.create_network()
+        >>> network = [network for network in driver.list_networks()
+        ...     if network.name == 'dummy-1'][0]
+        >>> driver.destroy_network(network)
+        True
+        >>> [network for network in driver.list_networks()
+        ...     if network.name == 'dummy-1']
+        []
+        """
+
+        self.nl.remove(network)
+        return True
+
+    def create_network(self, **kwargs):
+        """
+        Creates a dummy network; the network id is equal to the number of
+        networks in the network list.
+
+        >>> from libcloud.networking.drivers.dummy import DummyNetworkDriver
+        >>> driver = DummyNetworkDriver()
+        >>> sorted([network.name for network in driver.list_networks()])
+        []
+        >>> network = driver.create_network()
+        >>> sorted([network.name for network in driver.list_networks()])
+        ['dummy-1']
+        >>> driver.create_network().name
+        'dummy-2'
+        >>> driver.destroy_network(network)
+        True
+        >>> sorted([network.name for network in driver.list_networks()])
+        ['dummy-2']
+
+        See L{NetworkDriver.create_node} for more keyword args.
+        """
+        l = len(self.nl) + 1
+        n = Network(id=l,
+                 name='dummy-%d' % l,
+                 address=['127.0.%d.0' % l],
+                 size=255,
+                 driver=self,
+                 extra={'foo': 'bar'})
+        self.nl.append(n)
+        return n
+
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()

=== added file 'libcloud/networking/drivers/opennebula.py'
--- libcloud/networking/drivers/opennebula.py	1970-01-01 00:00:00 +0000
+++ libcloud/networking/drivers/opennebula.py	2011-11-13 06:09:54 +0000
@@ -0,0 +1,158 @@
+# Copyright 2002-2009, Distributed Systems Architecture Group, Universidad
+# Complutense de Madrid (dsa-research.org)
+#
+# 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.
+
+"""
+OpenNebula Driver.
+"""
+
+from xml.etree import ElementTree as ET
+from base64 import b64encode
+import httplib
+import hashlib
+
+from libcloud.common.base import ConnectionUserAndKey, XmlResponse
+from libcloud.networking.base import NetworkDriver, Network
+from libcloud.networking.types import Provider
+
+API_HOST = ''
+API_PORT = (4567, 443)
+API_SECURE = True
+DEFAULT_API_VERSION = '3.0'
+
+
+class OpenNebulaResponse(XmlResponse):
+    """
+    Response class for the OpenNebula driver.
+    """
+
+    def success(self):
+        i = int(self.status)
+        return i >= 200 and i <= 299
+
+    def parse_error(self):
+        if int(self.status) == httplib.UNAUTHORIZED:
+            raise InvalidCredsError(self.body)
+        return self.body
+
+
+class OpenNebulaConnection(ConnectionUserAndKey):
+    """
+    Connection class for the OpenNebula driver.
+    """
+
+    host = API_HOST
+    port = API_PORT
+    secure = API_SECURE
+    responseCls = OpenNebulaResponse
+
+    def add_default_headers(self, headers):
+        pass_sha1 = hashlib.sha1(self.key).hexdigest()
+        headers['Authorization'] = ('Basic %s' % b64encode('%s:%s' %
+                                                (self.user_id, pass_sha1)))
+        return headers
+
+
+class OpenNebulaNetworkDriver(NetworkDriver):
+    """
+    OpenNebula network driver.
+    """
+
+    connectionCls = OpenNebulaConnection
+    name = 'OpenNebula'
+    type = Provider.OPENNEBULA
+
+    def __new__(cls, key, secret=None, api_version=DEFAULT_API_VERSION,
+                **kwargs):
+        if cls is OpenNebulaNetworkDriver:
+            if api_version == '1.4' or api_version == '2.0' or \
+                api_version == '2.2' or api_version == '3.0':
+                cls = OpenNebula_1_4_NetworkDriver
+            else:
+                raise NotImplementedError(
+                    "No OpenNebulaNodeDriver found for API version %s" %
+                    (api_version))
+            return super(OpenNebulaNetworkDriver, cls).__new__(cls)
+
+    def list_networks(self):
+        return self._to_networks(self.connection.request('/network').object)
+
+    def destroy_network(self, network):
+        url = '/network/%s' % (str(network.id))
+        resp = self.connection.request(url, method='DELETE')
+
+        return resp.status == 204
+
+    def create_network(self, **kwargs):
+        """
+        Create a new network instance.
+
+        @keyword    public:   Boolean indicating whether the network is
+                              accessible to all users of the cloud provider
+                              (True || False).
+        @type       public:   C{bool}
+
+        See L{NetworkDriver.create_node} for more keyword args.
+        """
+        network = ET.Element('NETWORK')
+
+        name = ET.SubElement(network, 'NAME')
+        name.text = kwargs['name']
+
+        address = ET.SubElement(network, 'ADDRESS')
+        address.text = kwargs['address']
+
+        size = ET.SubElement(network, 'SIZE')
+        size.text = str(kwargs['size'])
+
+        if 'public' in kwargs:
+            public = ET.SubElement(network, 'PUBLIC')
+            if kwargs['public']:
+                public.text = 'yes'
+            else:
+                public.text = 'no'
+
+        xml = ET.tostring(network)
+        network = self.connection.request('/network', method='POST',
+                                       data=xml).object
+
+        return self._to_network(network)
+
+    def _to_networks(self, object):
+        networks = []
+        for element in object.findall('NETWORK'):
+            network_id = element.attrib['href'].partition('/network/')[2]
+            network_element = self.connection.request(('/network/%s' % (
+                                             network_id))).object
+            networks.append(self._to_network(network_element))
+
+        return networks
+
+    def _to_network(self, element):
+        return Network(id=element.findtext('ID'),
+                      name=element.findtext('NAME'),
+                      address=element.findtext('ADDRESS'),
+                      size=element.findtext('SIZE'),
+                      driver=self.connection.driver)
+
+
+class OpenNebula_1_4_NetworkDriver(OpenNebulaNetworkDriver):
+    """
+    OpenNebula network driver for OpenNebula v1.4 and greater.
+    """
+
+    pass

=== added file 'libcloud/networking/providers.py'
--- libcloud/networking/providers.py	1970-01-01 00:00:00 +0000
+++ libcloud/networking/providers.py	2011-11-12 06:57:18 +0000
@@ -0,0 +1,36 @@
+# 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.
+"""
+Provider related utilities
+"""
+
+from libcloud.utils import get_driver as _get_provider_driver
+from libcloud.networking.types import Provider
+
+__all__ = [
+    "Provider",
+    "DRIVERS",
+    "get_driver"]
+
+DRIVERS = {
+    Provider.DUMMY:
+        ('libcloud.networking.drivers.dummy', 'DummyNodeDriver'),
+    Provider.OPENNEBULA:
+        ('libcloud.networking.drivers.opennebula', 'OpenNebulaNetworkDriver'),
+}
+
+
+def get_driver(provider):
+    return _get_provider_driver(DRIVERS, provider)

=== added file 'libcloud/networking/types.py'
--- libcloud/networking/types.py	1970-01-01 00:00:00 +0000
+++ libcloud/networking/types.py	2011-11-07 08:11:49 +0000
@@ -0,0 +1,32 @@
+# 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.
+
+"""
+Base types used by other parts of libcloud.
+"""
+
+
+__all__ = ['Provider']
+
+
+class Provider(object):
+    """
+    Defines for each of the supported providers
+
+    @cvar DUMMY: Example provider
+    @cvar OPENNEBULA: OpenNebula.org
+    """
+    DUMMY = 0
+    OPENNEBULA = 16

=== modified file 'setup.py'
--- setup.py	2011-10-16 15:03:49 +0000
+++ setup.py	2011-11-13 05:28:44 +0000
@@ -30,9 +30,10 @@
 HTML_VIEWSOURCE_BASE = 'https://svn.apache.org/viewvc/libcloud/trunk'
 PROJECT_BASE_DIR = 'http://libcloud.apache.org'
 TEST_PATHS = ['test', 'test/common', 'test/compute', 'test/storage',
-              'test/loadbalancer', 'test/dns']
+              'test/networking', 'test/loadbalancer', 'test/dns']
 DOC_TEST_MODULES = [ 'libcloud.compute.drivers.dummy',
                      'libcloud.storage.drivers.dummy',
+                     'libcloud.networking.drivers.dummy',
                      'libcloud.dns.drivers.dummy' ]
 
 
@@ -202,6 +203,8 @@
         'libcloud.compute.drivers',
         'libcloud.storage',
         'libcloud.storage.drivers',
+        'libcloud.networking',
+        'libcloud.networking.drivers',
         'libcloud.drivers',
         'libcloud.loadbalancer',
         'libcloud.loadbalancer.drivers',

=== modified file 'test/__init__.py'
--- test/__init__.py	2011-10-12 21:00:33 +0000
+++ test/__init__.py	2011-11-09 04:29:16 +0000
@@ -46,6 +46,7 @@
                          'expected %d, but %d mock methods were executed'
                          % (expected, actual))
 
+
 class multipleresponse(object):
     """
     A decorator that allows MockHttp objects to return multi responses
@@ -65,7 +66,7 @@
 
 class MockResponse(object):
     """
-    A mock HTTPResponse
+    A mock HTTPResponse.
     """
     headers = {}
     body = StringIO()
@@ -91,6 +92,7 @@
     def msg(self):
         raise NotImplemented
 
+
 class BaseMockHttpObject(object):
     def _get_method_name(self, type, use_param, qs, path):
         meth_name = path.replace('/', '_').replace('.', '_').replace('-', '_')
@@ -101,6 +103,7 @@
             meth_name = '%s_%s' % (meth_name, param)
         return meth_name
 
+
 class MockHttp(BaseMockHttpObject):
     """
     A mock HTTP client/server suitable for testing purposes. This replaces
@@ -188,6 +191,7 @@
         return (httplib.FORBIDDEN, 'Oh Noes!', {'X-Foo': 'fail'},
                 httplib.responses[httplib.FORBIDDEN])
 
+
 class MockHttpTestCase(MockHttp, unittest.TestCase):
     # Same as the MockHttp class, but you can also use assertions in the
     # classes which inherit from this one.
@@ -200,6 +204,7 @@
     def runTest(self):
         pass
 
+
 class StorageMockHttp(MockHttp):
     def putrequest(self, method, action):
         pass
@@ -213,6 +218,7 @@
     def send(self, data):
         pass
 
+
 class MockRawResponse(BaseMockHttpObject):
     """
     Mock RawResponse object suitable for testing.
@@ -283,6 +289,7 @@
             return self
         return self._response
 
+
 if __name__ == "__main__":
     import doctest
     doctest.testmod()

=== modified file 'test/file_fixtures.py'
--- test/file_fixtures.py	2011-10-16 15:03:49 +0000
+++ test/file_fixtures.py	2011-11-13 05:27:56 +0000
@@ -21,6 +21,7 @@
 FIXTURES_ROOT = {
     'compute': 'compute/fixtures',
     'storage': 'storage/fixtures',
+    'networking': 'networking/fixtures',
     'loadbalancer': 'loadbalancer/fixtures',
     'dns': 'dns/fixtures',
     'openstack': 'compute/fixtures/openstack',
@@ -51,6 +52,11 @@
         super(StorageFileFixtures, self).__init__(fixtures_type='storage',
                                                   sub_dir=sub_dir)
 
+class NetworkingFileFixtures(FileFixtures):
+    def __init__(self, sub_dir=''):
+        super(NetworkingFileFixtures, self).__init__(fixtures_type='networking',
+                                                  sub_dir=sub_dir)
+
 class LoadBalancerFileFixtures(FileFixtures):
     def __init__(self, sub_dir=''):
         super(LoadBalancerFileFixtures, self).__init__(fixtures_type='loadbalancer',

=== added directory 'test/networking'
=== added file 'test/networking/__init__.py'
--- test/networking/__init__.py	1970-01-01 00:00:00 +0000
+++ test/networking/__init__.py	2011-11-13 05:30:05 +0000
@@ -0,0 +1,41 @@
+# 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.
+
+from libcloud.networking.base import Network
+
+
+class TestCaseMixin(object):
+
+    def test_list_networks_response(self):
+        networks = self.driver.list_networks()
+        self.assertTrue(isinstance(networks, list))
+        for network in networks:
+            self.assertTrue(isinstance(network, Network))
+
+    def test_destroy_network_response(self):
+        network = self.driver.list_networks()[0]
+        ret = self.driver.destroy_network(network)
+        self.assertTrue(isinstance(ret, bool))
+
+    def test_create_network_response(self):
+        network = self.driver.create_network(name='network-name',
+                                     address='192.168.0.1',
+                                     size=255)
+        self.assertTrue(isinstance(network, Network))
+
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()

=== added directory 'test/networking/fixtures'
=== added directory 'test/networking/fixtures/opennebula'
=== added file 'test/networking/fixtures/opennebula/network.xml'
--- test/networking/fixtures/opennebula/network.xml	1970-01-01 00:00:00 +0000
+++ test/networking/fixtures/opennebula/network.xml	2011-11-09 04:42:26 +0000
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<NETWORK href="http://www.opennebula.org/network/5">
+    <ID>5</ID>
+    <NAME>Network</NAME>
+    <ADDRESS>192.168.0.1</ADDRESS>
+    <SIZE>255</SIZE>
+</NETWORK>

=== added file 'test/networking/fixtures/opennebula/networks.xml'
--- test/networking/fixtures/opennebula/networks.xml	1970-01-01 00:00:00 +0000
+++ test/networking/fixtures/opennebula/networks.xml	2011-11-09 01:49:50 +0000
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<NETWORKS>  
+    <NETWORK href="http://www.opennebula.org/network/5" />  
+    <NETWORK href="http://www.opennebula.org/network/15" />
+</NETWORKS>

=== added file 'test/networking/test_base.py'
--- test/networking/test_base.py	1970-01-01 00:00:00 +0000
+++ test/networking/test_base.py	2011-11-13 05:30:21 +0000
@@ -0,0 +1,49 @@
+# 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.
+
+import unittest
+import sys
+
+from libcloud.common.base import ConnectionKey, ConnectionUserAndKey
+from libcloud.networking.base import Network, NetworkDriver
+from libcloud.common.base import Response
+
+from test import MockResponse           # pylint: disable-msg=E0611
+
+
+class FakeDriver(object):
+    type = 0
+
+
+class BaseTests(unittest.TestCase):
+
+    def test_base_network(self):
+        Network(id=0, name=0, address=0, size=0, driver=FakeDriver())
+
+    def test_base_response(self):
+        Response(MockResponse(status=200, body='foo'), ConnectionKey('foo'))
+
+    def test_base_network_driver(self):
+        NetworkDriver('foo')
+
+    def test_base_connection_key(self):
+        ConnectionKey('foo')
+
+    def test_base_connection_userkey(self):
+        ConnectionUserAndKey('foo', 'bar')
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())

=== added file 'test/networking/test_opennebula.py'
--- test/networking/test_opennebula.py	1970-01-01 00:00:00 +0000
+++ test/networking/test_opennebula.py	2011-11-13 06:19:08 +0000
@@ -0,0 +1,129 @@
+# Copyright 2002-2009, Distributed Systems Architecture Group, Universidad
+# Complutense de Madrid (dsa-research.org)
+#
+# 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.
+
+import unittest
+import httplib
+import sys
+
+from libcloud.common.types import InvalidCredsError, MalformedResponseError
+from libcloud.networking.providers import get_driver
+from libcloud.networking.types import Provider
+from libcloud.networking.drivers.opennebula import (
+    OpenNebulaNetworkDriver,
+    OpenNebula_1_4_NetworkDriver)
+from libcloud.networking.base import Network
+
+from test.file_fixtures import NetworkingFileFixtures
+from test.networking import TestCaseMixin
+from test import MockHttp
+
+from test.secrets import OPENNEBULA_PARAMS
+
+
+class OpenNebula_1_4_Tests(unittest.TestCase, TestCaseMixin):
+
+    driver_klass = OpenNebula_1_4_NetworkDriver
+    driver_args = OPENNEBULA_PARAMS
+    driver_kwargs = {}
+
+    @classmethod
+    def create_driver(self):
+        if self is not OpenNebula_1_4_FactoryMethodTests:
+            self.driver_type = self.driver_klass
+        return self.driver_type(*self.driver_args, **self.driver_kwargs)
+
+    def setUp(self):
+        OpenNebulaNetworkDriver.connectionCls.conn_classes = (None,
+                                                           OpenNebulaMockHttp)
+        self.driver = OpenNebulaNetworkDriver(*OPENNEBULA_PARAMS + ('1.4',))
+
+    def test_list_networks(self):
+        networks = self.driver.list_networks()
+        self.assertEqual(len(networks), 2)
+        network = networks[0]
+        self.assertEqual(network.id, '5')
+        self.assertEqual(network.name, 'Network')
+        self.assertEqual(network.address, '192.168.0.1')
+        self.assertEqual(network.size, '255')
+
+    def test_destroy_network(self):
+        network = Network(5, None, None, None, self.driver)
+        ret = self.driver.destroy_network(network)
+        self.assertTrue(ret)
+
+    def test_create_network(self):
+        network = self.driver.create_network(name='Network',
+            address='192.168.0.1', size='255', public=False)
+        self.assertEqual(network.id, '5')
+        self.assertEqual(network.name, 'Network')
+        self.assertEqual(network.address, '192.168.0.1')
+        self.assertEqual(network.size, '255')
+
+
+class OpenNebula_1_4_FactoryMethodTests(OpenNebula_1_4_Tests):
+
+    driver_klass = OpenNebula_1_4_NetworkDriver
+    driver_type = get_driver(Provider.OPENNEBULA)
+    driver_args = OPENNEBULA_PARAMS + ('1.0',)
+
+    def test_factory_method_invalid_version(self):
+        try:
+            self.driver_type(*(OPENNEBULA_PARAMS + ('0.5',)))
+        except NotImplementedError:
+            pass
+        else:
+            self.fail('Exception was not thrown')
+
+
+class OpenNebulaMockHttp(MockHttp):
+
+    fixtures = NetworkingFileFixtures('opennebula')
+
+    def _network(self, method, url, body, headers):
+        if method == 'GET':
+            body = self.fixtures.load('networks.xml')
+            return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+        if method == 'POST':
+            body = self.fixtures.load('network.xml')
+            return (httplib.CREATED, body, {},
+                    httplib.responses[httplib.CREATED])
+
+    def _network_5(self, method, url, body, headers):
+        if method == 'GET':
+            body = self.fixtures.load('network.xml')
+            return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+        if method == 'PUT':
+            body = ""
+            return (httplib.ACCEPTED, body, {},
+                    httplib.responses[httplib.ACCEPTED])
+
+        if method == 'DELETE':
+            body = ""
+            return (httplib.NO_CONTENT, body, {},
+                    httplib.responses[httplib.NO_CONTENT])
+
+    def _network_15(self, method, url, body, headers):
+        if method == 'GET':
+            body = self.fixtures.load('network.xml')
+            return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())

=== modified file 'test/test_file_fixtures.py'
--- test/test_file_fixtures.py	2011-10-12 21:00:33 +0000
+++ test/test_file_fixtures.py	2011-11-09 04:29:43 +0000
@@ -12,11 +12,13 @@
 # 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.
+
 import sys
 import unittest
 
 from test.file_fixtures import ComputeFileFixtures
 
+
 class FileFixturesTests(unittest.TestCase):
 
     def test_success(self):
@@ -27,5 +29,6 @@
         f = ComputeFileFixtures('meta')
         self.assertRaises(IOError, f.load, 'nil')
 
+
 if __name__ == '__main__':
     sys.exit(unittest.main())

