Add tests for volume attach and detach

This adds a test_attach_detach_volume case that attempts to create a
server, attach a volume and make sure that it hangs around during a
power cycle. It also tests to make sure that once we detach a volume,
it stays that way during a power cycle.

Also adds servers_client methods for start, stop, attach_volume and
detach_volume. Further adds get_partitions() to remote_client.

Note that right now, this doesn't succeed with libvirt. A patch to nova
is in the works.

Change-Id: Id43175fd9c8e8cf8971e77f212d0dc74a20def4d
diff --git a/tempest/common/utils/linux/remote_client.py b/tempest/common/utils/linux/remote_client.py
index 27f8fd3..e06aef5 100644
--- a/tempest/common/utils/linux/remote_client.py
+++ b/tempest/common/utils/linux/remote_client.py
@@ -46,3 +46,9 @@
         command = 'cat /proc/cpuinfo | grep processor | wc -l'
         output = self.ssh_client.exec_command(command)
         return int(output)
+
+    def get_partitions(self):
+        # Return the contents of /proc/partitions
+        command = 'cat /proc/partitions'
+        output = self.ssh_client.exec_command(command)
+        return output
diff --git a/tempest/services/nova/json/servers_client.py b/tempest/services/nova/json/servers_client.py
index 2c345f1..d6873e4 100644
--- a/tempest/services/nova/json/servers_client.py
+++ b/tempest/services/nova/json/servers_client.py
@@ -325,3 +325,31 @@
         resp, body = self.delete("servers/%s/metadata/%s" %
                                     (str(server_id), key))
         return resp, body
+
+    def stop(self, server_id):
+        post_body = json.dumps({'os-stop': None})
+        resp, body = self.post('servers/%s/action' % server_id,
+                               post_body, self.headers)
+
+    def start(self, server_id):
+        post_body = json.dumps({'os-start': None})
+        resp, body = self.post('servers/%s/action' % server_id,
+                               post_body, self.headers)
+
+    def attach_volume(self, server_id, volume_id, device='/dev/vdz'):
+        """Attaches a volume to a server instance"""
+        post_body = json.dumps(
+            {'volumeAttachment': {
+                    'volumeId': volume_id,
+                    'device': device,
+                    }
+             })
+        resp, body = self.post('servers/%s/os-volume_attachments' % server_id,
+                               post_body, self.headers)
+        return resp, body
+
+    def detach_volume(self, server_id, volume_id):
+        """Detaches a volume from a server instance"""
+        resp, body = self.delete('servers/%s/os-volume_attachments/%s' %
+                                 (server_id, volume_id))
+        return resp, body
diff --git a/tempest/tests/compute/test_attach_volume.py b/tempest/tests/compute/test_attach_volume.py
new file mode 100644
index 0000000..07bd7aa
--- /dev/null
+++ b/tempest/tests/compute/test_attach_volume.py
@@ -0,0 +1,110 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 IBM
+# All Rights Reserved.
+#
+#    Licensed 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 nose.plugins.attrib import attr
+import unittest2 as unittest
+
+import tempest.config
+from tempest.common.utils.data_utils import rand_name
+from tempest.common.utils.linux.remote_client import RemoteClient
+from tempest import openstack
+from tempest.tests.compute.base import BaseComputeTest
+
+
+class TestAttachVolume(BaseComputeTest):
+
+    run_ssh = tempest.config.TempestConfig().compute.run_ssh
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestAttachVolume, cls).setUpClass()
+        cls.device = 'vdb'
+
+    def _detach(self, server_id, volume_id):
+        self.servers_client.detach_volume(server_id, volume_id)
+        self.volumes_client.wait_for_volume_status(volume_id, 'available')
+
+    def _delete(self, server_id, volume_id):
+        self.volumes_client.delete_volume(volume_id)
+        self.servers_client.delete_server(server_id)
+
+    def _create_and_attach(self):
+        name = rand_name('server')
+
+        # Start a server and wait for it to become ready
+        resp, server = self.servers_client.create_server(name,
+                                                   self.image_ref,
+                                                   self.flavor_ref,
+                                                   adminPass='password')
+        self.servers_client.wait_for_server_status(server['id'], 'ACTIVE')
+
+        # Record addresses so that we can ssh later
+        resp, server['addresses'] = \
+            self.servers_client.list_addresses(server['id'])
+
+        # Create a volume and wait for it to become ready
+        resp, volume = self.volumes_client.create_volume(1,
+                                                         display_name='test')
+        self.volumes_client.wait_for_volume_status(volume['id'], 'available')
+
+        # Attach the volume to the server
+        self.servers_client.attach_volume(server['id'], volume['id'],
+                                    device='/dev/%s' % self.device)
+        self.volumes_client.wait_for_volume_status(volume['id'], 'in-use')
+
+        return server, volume
+
+    @attr(type='positive')
+    @unittest.skipIf(not run_ssh, 'SSH required for this test')
+    def test_attach_detach_volume(self):
+        """
+        Stop and Start a server with an attached volume, ensuring that
+        the volume remains attached.
+        """
+        server, volume = self._create_and_attach()
+
+        attached = True
+
+        try:
+            self.servers_client.stop(server['id'])
+            self.servers_client.wait_for_server_status(server['id'], 'SHUTOFF')
+
+            self.servers_client.start(server['id'])
+            self.servers_client.wait_for_server_status(server['id'], 'ACTIVE')
+
+            linux_client = RemoteClient(server,
+                                        self.ssh_user, server['adminPass'])
+            partitions = linux_client.get_partitions()
+            self.assertTrue(self.device in partitions)
+
+            self._detach(server['id'], volume['id'])
+            attached = False
+
+            self.servers_client.stop(server['id'])
+            self.servers_client.wait_for_server_status(server['id'], 'SHUTOFF')
+
+            self.servers_client.start(server['id'])
+            self.servers_client.wait_for_server_status(server['id'], 'ACTIVE')
+
+            linux_client = RemoteClient(server,
+                                        self.ssh_user, server['adminPass'])
+            partitions = linux_client.get_partitions()
+            self.assertFalse(self.device in partitions)
+        finally:
+            if attached:
+                self._detach(server['id'], volume['id'])
+            self._delete(server['id'], volume['id'])