libcloud-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From to...@apache.org
Subject svn commit: r1179108 - in /libcloud/trunk: libcloud/common/ libcloud/compute/drivers/ test/compute/ test/compute/fixtures/gandi/
Date Wed, 05 Oct 2011 08:48:24 GMT
Author: tomaz
Date: Wed Oct  5 08:48:23 2011
New Revision: 1179108

URL: http://svn.apache.org/viewvc?rev=1179108&view=rev
Log:
Add some extra methods to Gandi compute driver. This patch has been contributed by Aymeric Barantal <mric at gandi dot net> and is part of LIBCLOUD-115.

Added:
    libcloud/trunk/libcloud/common/gandi.py
    libcloud/trunk/test/compute/fixtures/gandi/disk_attach.xml
    libcloud/trunk/test/compute/fixtures/gandi/disk_create_from.xml
    libcloud/trunk/test/compute/fixtures/gandi/disk_detach.xml
    libcloud/trunk/test/compute/fixtures/gandi/disk_list.xml
    libcloud/trunk/test/compute/fixtures/gandi/disk_update.xml
    libcloud/trunk/test/compute/fixtures/gandi/iface_attach.xml
    libcloud/trunk/test/compute/fixtures/gandi/iface_detach.xml
    libcloud/trunk/test/compute/fixtures/gandi/iface_list.xml
Modified:
    libcloud/trunk/libcloud/compute/drivers/gandi.py
    libcloud/trunk/test/compute/test_gandi.py

Added: libcloud/trunk/libcloud/common/gandi.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/common/gandi.py?rev=1179108&view=auto
==============================================================================
--- libcloud/trunk/libcloud/common/gandi.py (added)
+++ libcloud/trunk/libcloud/common/gandi.py Wed Oct  5 08:48:23 2011
@@ -0,0 +1,217 @@
+# 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.
+"""
+Gandi driver base classes
+"""
+
+import time
+import hashlib
+import xmlrpclib
+
+import libcloud
+from libcloud.common.base import ConnectionKey
+from libcloud.compute.types import Provider, NodeState
+from libcloud.compute.base import NodeDriver, Node, \
+    NodeLocation, NodeSize, NodeImage
+
+# Global constants
+API_VERSION = '2.0'
+API_PREFIX = "https://rpc.gandi.net/xmlrpc/%s/" % API_VERSION
+
+DEFAULT_TIMEOUT = 600   # operation pooling max seconds
+DEFAULT_INTERVAL = 20   # seconds between 2 operation.info
+
+
+class GandiException(Exception):
+    """
+    Exception class for Gandi driver
+    """
+    def __str__(self):
+        return "(%u) %s" % (self.args[0], self.args[1])
+
+    def __repr__(self):
+        return "<GandiException code %u '%s'>" % (self.args[0], self.args[1])
+
+
+class GandiSafeTransport(xmlrpclib.SafeTransport):
+    pass
+
+
+class GandiTransport(xmlrpclib.Transport):
+    pass
+
+
+class GandiProxy(xmlrpclib.ServerProxy):
+    transportCls = (GandiTransport, GandiSafeTransport)
+
+    def __init__(self, user_agent, verbose=0):
+        cls = self.transportCls[0]
+        if API_PREFIX.startswith("https://"):
+            cls = self.transportCls[1]
+        t = cls(use_datetime=0)
+        t.user_agent = user_agent
+        xmlrpclib.ServerProxy.__init__(
+            self,
+            uri="%s" % (API_PREFIX),
+            transport=t,
+            verbose=verbose,
+            allow_none=True
+        )
+
+
+class GandiConnection(ConnectionKey):
+    """
+    Connection class for the Gandi driver
+    """
+
+    proxyCls = GandiProxy
+
+    def __init__(self, key, password=None):
+        super(GandiConnection, self).__init__(key)
+        self.driver = BaseGandiDriver
+
+        try:
+            self._proxy = self.proxyCls(self._user_agent())
+        except xmlrpclib.Fault, e:
+            raise GandiException(1000, e)
+
+    def request(self, method, *args):
+        """ Request xmlrpc method with given args"""
+        try:
+            return getattr(self._proxy, method)(self.key, *args)
+        except xmlrpclib.Fault, e:
+            raise GandiException(1001, e)
+
+
+class BaseGandiDriver(object):
+    """
+    Gandi base driver
+
+    """
+    connectionCls = GandiConnection
+    name = 'Gandi'
+
+    def __init__(self, key, secret=None, secure=False):
+        self.key = key
+        self.secret = secret
+        self.connection = self.connectionCls(key, secret)
+        self.connection.driver = self
+
+    # Specific methods for gandi
+    def _wait_operation(self, id, \
+        timeout=DEFAULT_TIMEOUT, check_interval=DEFAULT_INTERVAL):
+        """ Wait for an operation to succeed"""
+
+        for i in range(0, timeout, check_interval):
+            try:
+                op = self.connection.request('operation.info', int(id))
+
+                if op['step'] == 'DONE':
+                    return True
+                if op['step'] in  ['ERROR', 'CANCEL']:
+                    return False
+            except (KeyError, IndexError):
+                pass
+            except Exception, e:
+                raise GandiException(1002, e)
+
+            time.sleep(check_interval)
+        return False
+
+
+class BaseObject(object):
+    """Base class for objects not conventional"""
+
+    uuid_prefix = ''
+
+    def __init__(self, id, state, driver):
+        self.id = str(id) if id else None
+        self.state = state
+        self.driver = driver
+        self.uuid = self.get_uuid()
+
+    def get_uuid(self):
+        """Unique hash for this object
+
+        @return: C{string}
+
+        The hash is a function of an SHA1 hash of prefix, the object's ID and
+        its driver which means that it should be unique between all
+        interfaces.
+        TODO : to review
+        >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+        >>> driver = DummyNodeDriver(0)
+        >>> vif = driver.create_interface()
+        >>> vif.get_uuid()
+        'd3748461511d8b9b0e0bfa0d4d3383a619a2bb9f'
+
+        Note, for example, that this example will always produce the
+        same UUID!
+        """
+        return hashlib.sha1("%s:%s:%d" % \
+            (self.uuid_prefix, self.id, self.driver.type)).hexdigest()
+
+
+class IPAddress(BaseObject):
+    """
+    Provide a common interface for ip addresses
+    """
+
+    uuid_prefix = 'inet:'
+
+    def __init__(self, id, state, inet, driver, version=4, extra=None):
+        super(IPAddress, self).__init__(id, state, driver)
+        self.inet = inet
+        self.version = version
+        self.extra = extra or {}
+
+    def __repr__(self):
+        return (('<IPAddress: id=%s, address=%s, state=%s, driver=%s ...>')
+                % (self.id, self.inet, self.state, self.driver.name))
+
+
+class NetworkInterface(BaseObject):
+    """
+    Provide a common interface for network interfaces
+    """
+
+    uuid_prefix = 'if:'
+
+    def __init__(self, id, state, mac_address, driver,
+            ips=None, node_id=None, extra=None):
+        super(NetworkInterface, self).__init__(id, state, driver)
+        self.mac = mac_address
+        self.ips = ips or {}
+        self.node_id = node_id
+        self.extra = extra or {}
+
+    def __repr__(self):
+        return (('<Interface: id=%s, mac=%s, state=%s, driver=%s ...>')
+                % (self.id, self.mac, self.state, self.driver.name))
+
+
+class Disk(BaseObject):
+    """
+    Gandi disk component
+    """
+    def __init__(self, id, state, name, driver, size, extra=None):
+        super(Disk, self).__init__(id, state, driver)
+        self.name = name
+        self.size = size
+        self.extra = extra or {}
+
+    def __repr__(self):
+        return (('<Disk: id=%s, name=%s, state=%s, size=%s, driver=%s ...>')
+            % (self.id, self.name, self.state, self.size, self.driver.name))

Modified: libcloud/trunk/libcloud/compute/drivers/gandi.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/compute/drivers/gandi.py?rev=1179108&r1=1179107&r2=1179108&view=diff
==============================================================================
--- libcloud/trunk/libcloud/compute/drivers/gandi.py (original)
+++ libcloud/trunk/libcloud/compute/drivers/gandi.py Wed Oct  5 08:48:23 2011
@@ -13,149 +13,48 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 """
-Gandi driver
+Gandi driver for compute
 """
+from datetime import datetime
 
-import time
-import xmlrpclib
+from libcloud.common.gandi import BaseGandiDriver, GandiException, \
+    NetworkInterface, IPAddress, Disk
+from libcloud.compute.types import NodeState, Provider
+from libcloud.compute.base import Node, NodeDriver
+from libcloud.compute.base import NodeSize, NodeImage, NodeLocation
 
-import libcloud
-from libcloud.compute.types import Provider, NodeState
-from libcloud.compute.base import NodeDriver, Node, NodeLocation, NodeSize, NodeImage
-
-# Global constants
-API_VERSION = '2.0'
-API_PREFIX = "https://rpc.gandi.net/xmlrpc/%s/" % API_VERSION
-
-DEFAULT_TIMEOUT = 600   # operation pooling max seconds
-DEFAULT_INTERVAL = 20   # seconds between 2 operation.info
 
 NODE_STATE_MAP = {
     'running': NodeState.RUNNING,
     'halted': NodeState.TERMINATED,
     'paused': NodeState.TERMINATED,
-    'locked' : NodeState.TERMINATED,
-    'being_created' : NodeState.PENDING,
-    'invalid' : NodeState.UNKNOWN,
-    'legally_locked' : NodeState.PENDING,
-    'deleted' : NodeState.TERMINATED
+    'locked': NodeState.TERMINATED,
+    'being_created': NodeState.PENDING,
+    'invalid': NodeState.UNKNOWN,
+    'legally_locked': NodeState.PENDING,
+    'deleted': NodeState.TERMINATED
 }
 
 NODE_PRICE_HOURLY_USD = 0.02
 
-class GandiException(Exception):
-    """
-    Exception class for Gandi driver
-    """
-    def __str__(self):
-        return "(%u) %s" % (self.args[0], self.args[1])
-    def __repr__(self):
-        return "<GandiException code %u '%s'>" % (self.args[0], self.args[1])
-
-class GandiSafeTransport(xmlrpclib.SafeTransport):
-    pass
-
-class GandiTransport(xmlrpclib.Transport):
-    pass
-
-class GandiProxy(xmlrpclib.ServerProxy):
-    transportCls = (GandiTransport, GandiSafeTransport)
-
-    def __init__(self,user_agent, verbose=0):
-        cls = self.transportCls[0]
-        if API_PREFIX.startswith("https://"):
-            cls = self.transportCls[1]
-        t = cls(use_datetime=0)
-        t.user_agent = user_agent
-        xmlrpclib.ServerProxy.__init__(
-            self,
-            uri="%s" % (API_PREFIX),
-            transport=t,
-            verbose=verbose,
-            allow_none=True
-        )
-
-class GandiConnection(object):
-    """
-    Connection class for the Gandi driver
-    """
-
-    proxyCls = GandiProxy
-    driver = 'gandi'
-
-    def __init__(self, user, password=None):
-        self.ua = []
-
-        # Connect only with an api_key generated on website
-        self.api_key = user
-
-        try:
-            self._proxy = self.proxyCls(self._user_agent())
-        except xmlrpclib.Fault, e:
-            raise GandiException(1000, e)
-
-    def _user_agent(self):
-        return 'libcloud/%s (%s)%s' % (
-                libcloud.__version__,
-                self.driver,
-                "".join([" (%s)" % x for x in self.ua]))
-
-    def user_agent_append(self, s):
-        self.ua.append(s)
-
-    def request(self,method,*args):
-        """ Request xmlrpc method with given args"""
-        try:
-            return getattr(self._proxy, method)(self.api_key,*args)
-        except xmlrpclib.Fault, e:
-            raise GandiException(1001, e)
-
 
-class GandiNodeDriver(NodeDriver):
+class GandiNodeDriver(BaseGandiDriver, NodeDriver):
     """
     Gandi node driver
 
     """
-    connectionCls = GandiConnection
-    name = 'Gandi'
     api_name = 'gandi'
     friendly_name = 'Gandi.net'
     country = 'FR'
     type = Provider.GANDI
     # TODO : which features to enable ?
-    features = { }
-
-    def __init__(self, key, secret=None, secure=False):
-        self.key = key
-        self.secret = secret
-        self.connection = self.connectionCls(key, secret)
-        self.connection.driver = self
-
-    # Specific methods for gandi
-    def _wait_operation(self, id, timeout=DEFAULT_TIMEOUT, check_interval=DEFAULT_INTERVAL):
-        """ Wait for an operation to succeed"""
-
-        for i in range(0, timeout, check_interval):
-            try:
-                op = self.connection.request('operation.info', int(id))
-
-                if op['step'] == 'DONE':
-                    return True
-                if op['step'] in  ['ERROR','CANCEL']:
-                    return False
-            except (KeyError, IndexError):
-                pass
-            except Exception, e:
-                raise GandiException(1002, e)
-
-            time.sleep(check_interval)
-        return False
+    features = {}
 
-    def _node_info(self,id):
+    def _node_info(self, id):
         try:
-            obj = self.connection.request('vm.info',int(id))
+            obj = self.connection.request('vm.info', int(id))
             return obj
-        except Exception,e:
+        except Exception, e:
             raise GandiException(1003, e)
         return None
 
@@ -172,9 +71,9 @@ class GandiNodeDriver(NodeDriver):
             private_ip='',
             driver=self,
             extra={
-                'ai_active' : vm.get('ai_active'),
-                'datacenter_id' : vm.get('datacenter_id'),
-                'description' : vm.get('description')
+                'ai_active': vm.get('ai_active'),
+                'datacenter_id': vm.get('datacenter_id'),
+                'description': vm.get('description')
             }
         )
 
@@ -193,9 +92,9 @@ class GandiNodeDriver(NodeDriver):
         return nodes
 
     def reboot_node(self, node):
-        op = self.connection.request('vm.reboot',int(node.id))
+        op = self.connection.request('vm.reboot', int(node.id))
         op_res = self._wait_operation(op['id'])
-        vm = self.connection.request('vm.info',int(node.id))
+        vm = self.connection.request('vm.info', int(node.id))
         if vm['state'] == 'running':
             return True
         return False
@@ -204,18 +103,18 @@ class GandiNodeDriver(NodeDriver):
         vm = self._node_info(node.id)
         if vm['state'] == 'running':
             # Send vm_stop and wait for accomplish
-            op_stop = self.connection.request('vm.stop',int(node.id))
+            op_stop = self.connection.request('vm.stop', int(node.id))
             if not self._wait_operation(op_stop['id']):
                 raise GandiException(1010, 'vm.stop failed')
         # Delete
-        op = self.connection.request('vm.delete',int(node.id))
+        op = self.connection.request('vm.delete', int(node.id))
         if self._wait_operation(op['id']):
             return True
         return False
 
     def deploy_node(self, **kwargs):
-        raise NotImplementedError, \
-            'deploy_node not implemented for gandi driver'
+        raise NotImplementedError(
+            'deploy_node not implemented for gandi driver')
 
     def create_node(self, **kwargs):
         """Create a new Gandi node
@@ -234,7 +133,7 @@ class GandiNodeDriver(NodeDriver):
                             (required)
         @type       size:   L{NodeSize}
 
-        @keyword    login:  user name to create for login on this machine (required)
+        @keyword    login:  user name to create for login on machine (required)
         @type       login: String
 
         @keyword    password: password for user that'll be created (required)
@@ -245,17 +144,20 @@ class GandiNodeDriver(NodeDriver):
         """
 
         if kwargs.get('login') is None or kwargs.get('password') is None:
-            raise GandiException(1020, 'login and password must be defined for node creation')
+            raise GandiException(1020,
+                'login and password must be defined for node creation')
 
         location = kwargs.get('location')
-        if location and isinstance(location,NodeLocation):
+        if location and isinstance(location, NodeLocation):
             dc_id = int(location.id)
         else:
-            raise GandiException(1021, 'location must be a subclass of NodeLocation')
+            raise GandiException(1021,
+                'location must be a subclass of NodeLocation')
 
         size = kwargs.get('size')
-        if not size and not isinstance(size,NodeSize):
-            raise GandiException(1022, 'size must be a subclass of NodeSize')
+        if not size and not isinstance(size, NodeSize):
+            raise GandiException(1022,
+                'size must be a subclass of NodeSize')
 
         src_disk_id = int(kwargs['image'].id)
 
@@ -271,20 +173,21 @@ class GandiNodeDriver(NodeDriver):
             'password': kwargs['password'],  # TODO : use NodeAuthPassword
             'memory': int(size.ram),
             'cores': int(size.id),
-            'bandwidth' : int(size.bandwidth),
-            'ip_version':  kwargs.get('inet_family',4),
+            'bandwidth': int(size.bandwidth),
+            'ip_version':  kwargs.get('inet_family', 4),
             }
 
         # Call create_from helper api. Return 3 operations : disk_create,
         # iface_create,vm_create
-        (op_disk,op_iface,op_vm) = self.connection.request(
+        (op_disk, op_iface, op_vm) = self.connection.request(
             'vm.create_from',
-            vm_spec,disk_spec,src_disk_id
+            vm_spec, disk_spec, src_disk_id
         )
 
         # We wait for vm_create to finish
         if self._wait_operation(op_vm['id']):
-            # after successful operation, get ip information thru first interface
+            # after successful operation, get ip information
+            # thru first interface
             node = self._node_info(op_vm['vm_id'])
             ifaces = node.get('ifaces')
             if len(ifaces) > 0:
@@ -305,10 +208,10 @@ class GandiNodeDriver(NodeDriver):
     def list_images(self, location=None):
         try:
             if location:
-                filtering = { 'datacenter_id' : int(location.id) }
+                filtering = {'datacenter_id': int(location.id)}
             else:
                 filtering = {}
-            images = self.connection.request('image.list', filtering )
+            images = self.connection.request('image.list', filtering)
             return [self._to_image(i) for i in images]
         except Exception, e:
             raise GandiException(1011, e)
@@ -340,8 +243,8 @@ class GandiNodeDriver(NodeDriver):
             if available_res['servers'] < 1:
                 # No server quota, no way
                 return shares
-            for i in range(1,max_core + 1):
-                share = {id:i}
+            for i in range(1, max_core + 1):
+                share = {id: i}
                 share_is_available = True
                 for k in ['memory', 'disk', 'bandwidth']:
                     if share_def[k] * i > available_res[k]:
@@ -351,7 +254,7 @@ class GandiNodeDriver(NodeDriver):
                         share[k] = share_def[k] * i
                 if share_is_available:
                     nb_core = i
-                    shares.append(self._to_size(nb_core,share))
+                    shares.append(self._to_size(nb_core, share))
             return shares
 
     def _to_loc(self, loc):
@@ -365,3 +268,131 @@ class GandiNodeDriver(NodeDriver):
     def list_locations(self):
         res = self.connection.request("datacenter.list")
         return [self._to_loc(l) for l in res]
+
+    def _to_iface(self, iface):
+        ips = []
+        for ip in iface.get('ips', []):
+            new_ip = IPAddress(
+                ip['id'],
+                NODE_STATE_MAP.get(
+                    ip['state'],
+                    NodeState.UNKNOWN
+                ),
+                ip['ip'],
+                self.connection.driver,
+                version=ip.get('version'),
+                extra={'reverse': ip['reverse']}
+                )
+            ips.append(new_ip)
+        return NetworkInterface(
+            iface['id'],
+            NODE_STATE_MAP.get(
+                iface['state'],
+                NodeState.UNKNOWN
+            ),
+            mac_address=None,
+            driver=self.connection.driver,
+            ips=ips,
+            node_id=iface.get('vm_id'),
+            extra={'bandwidth': iface['bandwidth']},
+        )
+
+    def _to_ifaces(self, ifaces):
+        return [self._to_iface(i) for i in ifaces]
+
+    def ex_list_interfaces(self):
+        """Specific method to list network interfaces"""
+        ifaces = self.connection.request('iface.list')
+        ips = self.connection.request('ip.list')
+        for iface in ifaces:
+            iface['ips'] = filter(lambda i: i['iface_id'] == iface['id'], ips)
+        return self._to_ifaces(ifaces)
+
+    def _to_disk(self, element):
+        disk = Disk(
+            id=element['id'],
+            state=NODE_STATE_MAP.get(
+                element['state'],
+                NodeState.UNKNOWN
+            ),
+            name=element['name'],
+            driver=self.connection.driver,
+            size=element['size'],
+            extra={'can_snapshot': element['can_snapshot']}
+        )
+        return disk
+
+    def _to_disks(self, elements):
+        return [self._to_disk(el) for el in elements]
+
+    def ex_list_disks(self):
+        """Specific method to list all disk"""
+        res = self.connection.request('disk.list', {})
+        disks = []
+        return self._to_disks(res)
+
+    def ex_node_attach_disk(self, node, disk):
+        """Specific method to attach a disk to a node"""
+        op = self.connection.request('vm.disk_attach',
+            int(node.id), int(disk.id))
+        if self._wait_operation(op['id']):
+            return True
+        return False
+
+    def ex_node_detach_disk(self, node, disk):
+        """Specific method to detach a disk from a node"""
+        op = self.connection.request('vm.disk_detach',
+            int(node.id), int(disk.id))
+        if self._wait_operation(op['id']):
+            return True
+        return False
+
+    def ex_node_attach_interface(self, node, iface):
+        """Specific method to attach an interface to a node"""
+        op = self.connection.request('vm.iface_attach',
+            int(node.id), int(iface.id))
+        if self._wait_operation(op['id']):
+            return True
+        return False
+
+    def ex_node_detach_interface(self, node, iface):
+        """Specific method to detach an interface from a node"""
+        op = self.connection.request('vm.iface_detach',
+            int(node.id), int(iface.id))
+        if self._wait_operation(op['id']):
+            return True
+        return False
+
+    def ex_snapshot_disk(self, disk, name=None):
+        """Specific method to make a snapshot of a disk"""
+        if not disk.extra.get('can_snapshot'):
+            raise GandiException(1021, "Disk %s can't snapshot" % disk.id)
+        if not name:
+            suffix = datetime.today().strftime("%Y%m%d")
+            name = "snap_%s" % (suffix)
+        op = self.connection.request('disk.create_from',
+            {
+                'name': name,
+                'type': 'snapshot',
+            },
+            int(disk.id),
+            )
+        if self._wait_operation(op['id']):
+            return True
+        return False
+
+    def ex_update_disk(self, disk, new_size=None, new_name=None):
+        """Specific method to update size or name of a disk
+        WARNING: if a server is attached it'll be rebooted
+        """
+        params = {}
+        if new_size:
+            params.update({'size': new_size})
+        if new_name:
+            params.update({'name': new_name})
+        op = self.connection.request('disk.update',
+            int(disk.id),
+            params)
+        if self._wait_operation(op['id']):
+            return True
+        return False

Added: libcloud/trunk/test/compute/fixtures/gandi/disk_attach.xml
URL: http://svn.apache.org/viewvc/libcloud/trunk/test/compute/fixtures/gandi/disk_attach.xml?rev=1179108&view=auto
==============================================================================
--- libcloud/trunk/test/compute/fixtures/gandi/disk_attach.xml (added)
+++ libcloud/trunk/test/compute/fixtures/gandi/disk_attach.xml Wed Oct  5 08:48:23 2011
@@ -0,0 +1,53 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>iface_id</name>
+<value><string></string></value>
+</member>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20110921T12:57:05</dateTime.iso8601></value>
+</member>
+<member>
+<name>vm_id</name>
+<value><int>250133</int></value>
+</member>
+<member>
+<name>date_start</name>
+<value><string></string></value>
+</member>
+<member>
+<name>disk_id</name>
+<value><int>34918</int></value>
+</member>
+<member>
+<name>source</name>
+<value><string>AB3917-GANDI</string></value>
+</member>
+<member>
+<name>step</name>
+<value><string>WAIT</string></value>
+</member>
+<member>
+<name>ip_id</name>
+<value><string></string></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20110921T12:57:05</dateTime.iso8601></value>
+</member>
+<member>
+<name>type</name>
+<value><string>disk_attach</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>657982</int></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
\ No newline at end of file

Added: libcloud/trunk/test/compute/fixtures/gandi/disk_create_from.xml
URL: http://svn.apache.org/viewvc/libcloud/trunk/test/compute/fixtures/gandi/disk_create_from.xml?rev=1179108&view=auto
==============================================================================
--- libcloud/trunk/test/compute/fixtures/gandi/disk_create_from.xml (added)
+++ libcloud/trunk/test/compute/fixtures/gandi/disk_create_from.xml Wed Oct  5 08:48:23 2011
@@ -0,0 +1,53 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>iface_id</name>
+<value><string></string></value>
+</member>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20110921T14:20:56</dateTime.iso8601></value>
+</member>
+<member>
+<name>vm_id</name>
+<value><string></string></value>
+</member>
+<member>
+<name>date_start</name>
+<value><string></string></value>
+</member>
+<member>
+<name>disk_id</name>
+<value><int>35288</int></value>
+</member>
+<member>
+<name>source</name>
+<value><string>AB3917-GANDI</string></value>
+</member>
+<member>
+<name>step</name>
+<value><string>WAIT</string></value>
+</member>
+<member>
+<name>ip_id</name>
+<value><string></string></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20110921T14:20:56</dateTime.iso8601></value>
+</member>
+<member>
+<name>type</name>
+<value><string>disk_create</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>657985</int></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
\ No newline at end of file

Added: libcloud/trunk/test/compute/fixtures/gandi/disk_detach.xml
URL: http://svn.apache.org/viewvc/libcloud/trunk/test/compute/fixtures/gandi/disk_detach.xml?rev=1179108&view=auto
==============================================================================
--- libcloud/trunk/test/compute/fixtures/gandi/disk_detach.xml (added)
+++ libcloud/trunk/test/compute/fixtures/gandi/disk_detach.xml Wed Oct  5 08:48:23 2011
@@ -0,0 +1,53 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>iface_id</name>
+<value><string></string></value>
+</member>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20110921T12:57:35</dateTime.iso8601></value>
+</member>
+<member>
+<name>vm_id</name>
+<value><int>250133</int></value>
+</member>
+<member>
+<name>date_start</name>
+<value><string></string></value>
+</member>
+<member>
+<name>disk_id</name>
+<value><int>34918</int></value>
+</member>
+<member>
+<name>source</name>
+<value><string>AB3917-GANDI</string></value>
+</member>
+<member>
+<name>step</name>
+<value><string>WAIT</string></value>
+</member>
+<member>
+<name>ip_id</name>
+<value><string></string></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20110921T12:57:35</dateTime.iso8601></value>
+</member>
+<member>
+<name>type</name>
+<value><string>disk_detach</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>657983</int></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
\ No newline at end of file

Added: libcloud/trunk/test/compute/fixtures/gandi/disk_list.xml
URL: http://svn.apache.org/viewvc/libcloud/trunk/test/compute/fixtures/gandi/disk_list.xml?rev=1179108&view=auto
==============================================================================
--- libcloud/trunk/test/compute/fixtures/gandi/disk_list.xml (added)
+++ libcloud/trunk/test/compute/fixtures/gandi/disk_list.xml Wed Oct  5 08:48:23 2011
@@ -0,0 +1,200 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<methodResponse>
+<params>
+<param>
+<value><array><data>
+<value><struct>
+<member>
+<name>datacenter_id</name>
+<value><int>1</int></value>
+</member>
+<member>
+<name>name</name>
+<value><string>disk_libcloud2</string></value>
+</member>
+<member>
+<name>kernel_version</name>
+<value><string>2.6.32</string></value>
+</member>
+<member>
+<name>can_snapshot</name>
+<value><boolean>0</boolean></value>
+</member>
+<member>
+<name>visibility</name>
+<value><string>private</string></value>
+</member>
+<member>
+<name>label</name>
+<value><string>Debian 5</string></value>
+</member>
+<member>
+<name>vms_id</name>
+<value><array><data>
+</data></array></value>
+</member>
+<member>
+<name>source</name>
+<value><int>23351</int></value>
+</member>
+<member>
+<name>state</name>
+<value><string>created</string></value>
+</member>
+<member>
+<name>is_boot_disk</name>
+<value><boolean>0</boolean></value>
+</member>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20101116T10:51:59</dateTime.iso8601></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20101028T13:52:38</dateTime.iso8601></value>
+</member>
+<member>
+<name>type</name>
+<value><string>data</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>34918</int></value>
+</member>
+<member>
+<name>size</name>
+<value><int>3072</int></value>
+</member>
+</struct></value>
+<value><struct>
+<member>
+<name>datacenter_id</name>
+<value><int>1</int></value>
+</member>
+<member>
+<name>name</name>
+<value><string>test1</string></value>
+</member>
+<member>
+<name>kernel_version</name>
+<value><string>2.6.32</string></value>
+</member>
+<member>
+<name>can_snapshot</name>
+<value><string></string></value>
+</member>
+<member>
+<name>visibility</name>
+<value><string>private</string></value>
+</member>
+<member>
+<name>label</name>
+<value><string>Debian 5</string></value>
+</member>
+<member>
+<name>vms_id</name>
+<value><array><data>
+<value><int>250133</int></value>
+</data></array></value>
+</member>
+<member>
+<name>source</name>
+<value><int>23351</int></value>
+</member>
+<member>
+<name>state</name>
+<value><string>created</string></value>
+</member>
+<member>
+<name>is_boot_disk</name>
+<value><boolean>1</boolean></value>
+</member>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20110120T15:02:01</dateTime.iso8601></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20110120T14:57:55</dateTime.iso8601></value>
+</member>
+<member>
+<name>type</name>
+<value><string>data</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>34951</int></value>
+</member>
+<member>
+<name>size</name>
+<value><int>3072</int></value>
+</member>
+</struct></value>
+<value><struct>
+<member>
+<name>datacenter_id</name>
+<value><int>1</int></value>
+</member>
+<member>
+<name>name</name>
+<value><string>test_disk</string></value>
+</member>
+<member>
+<name>kernel_version</name>
+<value><string>2.6.32</string></value>
+</member>
+<member>
+<name>can_snapshot</name>
+<value><boolean>1</boolean></value>
+</member>
+<member>
+<name>visibility</name>
+<value><string>private</string></value>
+</member>
+<member>
+<name>label</name>
+<value><string>Debian 5</string></value>
+</member>
+<member>
+<name>vms_id</name>
+<value><array><data>
+<value><int>250288</int></value>
+</data></array></value>
+</member>
+<member>
+<name>source</name>
+<value><int>23351</int></value>
+</member>
+<member>
+<name>state</name>
+<value><string>created</string></value>
+</member>
+<member>
+<name>is_boot_disk</name>
+<value><boolean>1</boolean></value>
+</member>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20110325T16:31:11</dateTime.iso8601></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20110324T17:14:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>type</name>
+<value><string>data</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>35170</int></value>
+</member>
+<member>
+<name>size</name>
+<value><int>3072</int></value>
+</member>
+</struct></value>
+</data></array></value>
+</param>
+</params>
+</methodResponse>
\ No newline at end of file

Added: libcloud/trunk/test/compute/fixtures/gandi/disk_update.xml
URL: http://svn.apache.org/viewvc/libcloud/trunk/test/compute/fixtures/gandi/disk_update.xml?rev=1179108&view=auto
==============================================================================
--- libcloud/trunk/test/compute/fixtures/gandi/disk_update.xml (added)
+++ libcloud/trunk/test/compute/fixtures/gandi/disk_update.xml Wed Oct  5 08:48:23 2011
@@ -0,0 +1,53 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>iface_id</name>
+<value><string></string></value>
+</member>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20110921T14:23:10</dateTime.iso8601></value>
+</member>
+<member>
+<name>vm_id</name>
+<value><string></string></value>
+</member>
+<member>
+<name>date_start</name>
+<value><string></string></value>
+</member>
+<member>
+<name>disk_id</name>
+<value><int>34951</int></value>
+</member>
+<member>
+<name>source</name>
+<value><string>AB3917-GANDI</string></value>
+</member>
+<member>
+<name>step</name>
+<value><string>WAIT</string></value>
+</member>
+<member>
+<name>ip_id</name>
+<value><string></string></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20110921T14:23:10</dateTime.iso8601></value>
+</member>
+<member>
+<name>type</name>
+<value><string>disk_update</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>657987</int></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
\ No newline at end of file

Added: libcloud/trunk/test/compute/fixtures/gandi/iface_attach.xml
URL: http://svn.apache.org/viewvc/libcloud/trunk/test/compute/fixtures/gandi/iface_attach.xml?rev=1179108&view=auto
==============================================================================
--- libcloud/trunk/test/compute/fixtures/gandi/iface_attach.xml (added)
+++ libcloud/trunk/test/compute/fixtures/gandi/iface_attach.xml Wed Oct  5 08:48:23 2011
@@ -0,0 +1,53 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>iface_id</name>
+<value><int>7857</int></value>
+</member>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20110921T12:49:35</dateTime.iso8601></value>
+</member>
+<member>
+<name>vm_id</name>
+<value><int>250133</int></value>
+</member>
+<member>
+<name>date_start</name>
+<value><string></string></value>
+</member>
+<member>
+<name>disk_id</name>
+<value><string></string></value>
+</member>
+<member>
+<name>source</name>
+<value><string>AB3917-GANDI</string></value>
+</member>
+<member>
+<name>step</name>
+<value><string>WAIT</string></value>
+</member>
+<member>
+<name>ip_id</name>
+<value><string></string></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20110921T12:49:35</dateTime.iso8601></value>
+</member>
+<member>
+<name>type</name>
+<value><string>iface_attach</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>657980</int></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
\ No newline at end of file

Added: libcloud/trunk/test/compute/fixtures/gandi/iface_detach.xml
URL: http://svn.apache.org/viewvc/libcloud/trunk/test/compute/fixtures/gandi/iface_detach.xml?rev=1179108&view=auto
==============================================================================
--- libcloud/trunk/test/compute/fixtures/gandi/iface_detach.xml (added)
+++ libcloud/trunk/test/compute/fixtures/gandi/iface_detach.xml Wed Oct  5 08:48:23 2011
@@ -0,0 +1,53 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>iface_id</name>
+<value><int>7857</int></value>
+</member>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20110921T12:53:29</dateTime.iso8601></value>
+</member>
+<member>
+<name>vm_id</name>
+<value><int>250133</int></value>
+</member>
+<member>
+<name>date_start</name>
+<value><string></string></value>
+</member>
+<member>
+<name>disk_id</name>
+<value><string></string></value>
+</member>
+<member>
+<name>source</name>
+<value><string>AB3917-GANDI</string></value>
+</member>
+<member>
+<name>step</name>
+<value><string>WAIT</string></value>
+</member>
+<member>
+<name>ip_id</name>
+<value><string></string></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20110921T12:53:29</dateTime.iso8601></value>
+</member>
+<member>
+<name>type</name>
+<value><string>iface_detach</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>657981</int></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
\ No newline at end of file

Added: libcloud/trunk/test/compute/fixtures/gandi/iface_list.xml
URL: http://svn.apache.org/viewvc/libcloud/trunk/test/compute/fixtures/gandi/iface_list.xml?rev=1179108&view=auto
==============================================================================
--- libcloud/trunk/test/compute/fixtures/gandi/iface_list.xml (added)
+++ libcloud/trunk/test/compute/fixtures/gandi/iface_list.xml Wed Oct  5 08:48:23 2011
@@ -0,0 +1,99 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<methodResponse>
+<params>
+<param>
+<value><array><data>
+<value><struct>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20110120T14:58:44</dateTime.iso8601></value>
+</member>
+<member>
+<name>vm_id</name>
+<value><int>250133</int></value>
+</member>
+<member>
+<name>bandwidth</name>
+<value><double>5120.0</double></value>
+</member>
+<member>
+<name>datacenter_id</name>
+<value><int>1</int></value>
+</member>
+<member>
+<name>state</name>
+<value><string>used</string></value>
+</member>
+<member>
+<name>num</name>
+<value><int>0</int></value>
+</member>
+<member>
+<name>ips_id</name>
+<value><array><data>
+<value><int>9256</int></value>
+<value><int>9294</int></value>
+</data></array></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20110120T14:57:55</dateTime.iso8601></value>
+</member>
+<member>
+<name>type</name>
+<value><string>public</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>7857</int></value>
+</member>
+</struct></value>
+<value><struct>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20110324T17:14:16</dateTime.iso8601></value>
+</member>
+<member>
+<name>vm_id</name>
+<value><int>250288</int></value>
+</member>
+<member>
+<name>bandwidth</name>
+<value><double>5192.0</double></value>
+</member>
+<member>
+<name>datacenter_id</name>
+<value><int>1</int></value>
+</member>
+<member>
+<name>state</name>
+<value><string>used</string></value>
+</member>
+<member>
+<name>num</name>
+<value><int>0</int></value>
+</member>
+<member>
+<name>ips_id</name>
+<value><array><data>
+<value><int>9298</int></value>
+<value><int>9508</int></value>
+</data></array></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20110324T17:14:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>type</name>
+<value><string>public</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>8019</int></value>
+</member>
+</struct></value>
+</data></array></value>
+</param>
+</params>
+</methodResponse>
\ No newline at end of file

Modified: libcloud/trunk/test/compute/test_gandi.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/test/compute/test_gandi.py?rev=1179108&r1=1179107&r2=1179108&view=diff
==============================================================================
--- libcloud/trunk/test/compute/test_gandi.py (original)
+++ libcloud/trunk/test/compute/test_gandi.py Wed Oct  5 08:48:23 2011
@@ -21,6 +21,7 @@ import httplib
 import xmlrpclib
 
 from libcloud.compute.drivers.gandi import GandiNodeDriver as Gandi
+from libcloud.common.gandi import GandiException
 from libcloud.compute.types import NodeState
 
 from xml.etree import ElementTree as ET
@@ -28,6 +29,7 @@ from test import MockHttp
 from test.file_fixtures import ComputeFileFixtures
 from test.secrets import GANDI_PARAMS
 
+
 class MockGandiTransport(xmlrpclib.Transport):
 
     def request(self, host, handler, request_body, verbose=0):
@@ -43,11 +45,14 @@ class MockGandiTransport(xmlrpclib.Trans
             response = self.parse_response(resp.body)
         return response
 
+
 class GandiTests(unittest.TestCase):
 
     node_name = 'test2'
+
     def setUp(self):
-        Gandi.connectionCls.proxyCls.transportCls = [MockGandiTransport, MockGandiTransport]
+        Gandi.connectionCls.proxyCls.transportCls = \
+            [MockGandiTransport, MockGandiTransport]
         self.driver = Gandi(*GANDI_PARAMS)
 
     def test_list_nodes(self):
@@ -55,11 +60,13 @@ class GandiTests(unittest.TestCase):
         self.assertTrue(len(nodes) > 0)
 
     def test_list_locations(self):
-        loc = filter(lambda x: 'france' in x.country.lower(), self.driver.list_locations())[0]
+        loc = filter(lambda x: 'france' in x.country.lower(),
+            self.driver.list_locations())[0]
         self.assertEqual(loc.country, 'France')
 
     def test_list_images(self):
-        loc = filter(lambda x: 'france' in x.country.lower(), self.driver.list_locations())[0]
+        loc = filter(lambda x: 'france' in x.country.lower(),
+            self.driver.list_locations())[0]
         images = self.driver.list_images(loc)
         self.assertTrue(len(images) > 2)
 
@@ -84,18 +91,64 @@ class GandiTests(unittest.TestCase):
 
     def test_create_node(self):
         login = 'libcloud'
-        passwd = ''.join(random.choice(string.letters + string.digits) for i in xrange(10))
+        passwd = ''.join(random.choice(string.letters + string.digits)
+            for i in xrange(10))
         # Get france datacenter
-        loc = filter(lambda x: 'france' in x.country.lower(), self.driver.list_locations())[0]
+        loc = filter(lambda x: 'france' in x.country.lower(),
+            self.driver.list_locations())[0]
         # Get a debian image
         images = self.driver.list_images(loc)
         images = [x for x in images if x.name.lower().startswith('debian')]
         img = filter(lambda x: '5' in x.name, images)[0]
         # Get a configuration size
         size = self.driver.list_sizes()[0]
-        node = self.driver.create_node(name=self.node_name, login=login, password=passwd, image=img, location=loc, size=size)
+        node = self.driver.create_node(name=self.node_name, login=login,
+            password=passwd, image=img, location=loc, size=size)
         self.assertEqual(node.name, self.node_name)
 
+    def test_ex_list_disks(self):
+        disks = self.driver.ex_list_disks()
+        self.assertTrue(len(disks) > 0)
+
+    def test_ex_list_interfaces(self):
+        ifaces = self.driver.ex_list_interfaces()
+        self.assertTrue(len(ifaces) > 0)
+
+    def test_ex_attach_interface(self):
+        ifaces = self.driver.ex_list_interfaces()
+        nodes = self.driver.list_nodes()
+        res = self.driver.ex_node_attach_interface(nodes[0], ifaces[0])
+        self.assertTrue(res)
+
+    def test_ex_detach_interface(self):
+        ifaces = self.driver.ex_list_interfaces()
+        nodes = self.driver.list_nodes()
+        res = self.driver.ex_node_detach_interface(nodes[0], ifaces[0])
+        self.assertTrue(res)
+
+    def test_ex_attach_disk(self):
+        disks = self.driver.ex_list_disks()
+        nodes = self.driver.list_nodes()
+        res = self.driver.ex_node_attach_disk(nodes[0], disks[0])
+        self.assertTrue(res)
+
+    def test_ex_detach_disk(self):
+        disks = self.driver.ex_list_disks()
+        nodes = self.driver.list_nodes()
+        res = self.driver.ex_node_detach_disk(nodes[0], disks[0])
+        self.assertTrue(res)
+
+    def test_ex_snapshot_disk(self):
+        disks = self.driver.ex_list_disks()
+        self.assertTrue(self.driver.ex_snapshot_disk(disks[2]))
+        self.assertRaises(GandiException,
+            self.driver.ex_snapshot_disk, disks[0])
+
+    def test_ex_update_disk(self):
+        disks = self.driver.ex_list_disks()
+        self.assertTrue(self.driver.ex_update_disk(disks[0], new_size=4096))
+
+
 class GandiMockHttp(MockHttp):
 
     fixtures = ComputeFileFixtures('gandi')
@@ -144,5 +197,37 @@ class GandiMockHttp(MockHttp):
         body = self.fixtures.load('vm_stop.xml')
         return (httplib.OK, body, {}, httplib.responses[httplib.OK])
 
+    def _xmlrpc_2_0__iface_list(self, method, url, body, headers):
+        body = self.fixtures.load('iface_list.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _xmlrpc_2_0__disk_list(self, method, url, body, headers):
+        body = self.fixtures.load('disk_list.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _xmlrpc_2_0__vm_iface_attach(self, method, url, body, headers):
+        body = self.fixtures.load('iface_attach.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _xmlrpc_2_0__vm_iface_detach(self, method, url, body, headers):
+            body = self.fixtures.load('iface_detach.xml')
+            return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _xmlrpc_2_0__vm_disk_attach(self, method, url, body, headers):
+        body = self.fixtures.load('disk_attach.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _xmlrpc_2_0__vm_disk_detach(self, method, url, body, headers):
+            body = self.fixtures.load('disk_detach.xml')
+            return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _xmlrpc_2_0__disk_create_from(self, method, url, body, headers):
+            body = self.fixtures.load('disk_create_from.xml')
+            return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _xmlrpc_2_0__disk_update(self, method, url, body, headers):
+            body = self.fixtures.load('disk_update.xml')
+            return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
 if __name__ == '__main__':
     sys.exit(unittest.main())



Mime
View raw message