From 61d88463da5eb65734edbbdad13378af6ce0f641 Mon Sep 17 00:00:00 2001
From: Chris Yeoh <cyeoh@au1.ibm.com>
Date: Tue, 24 Dec 2013 00:28:13 +1030
Subject: [PATCH] Adds volume support for the V3 API

Adds the ability to attach, detach and swap volumes on
servers. There is no code shared with the v1_1 version
because for V3 the volumes interface is completely different
and the attach/detach/swap functionality is simply a server
action rather than something accessed through a special resource.

Partially implements blueprint v3-api

Change-Id: Ib405f821fe557745d11cff9db08381fc15233fe5
---
 novaclient/tests/v3/fakes.py        |  5 ++-
 novaclient/tests/v3/test_volumes.py | 47 ++++++++++++++++++++
 novaclient/v3/client.py             |  2 +
 novaclient/v3/shell.py              | 19 +++++++-
 novaclient/v3/volumes.py            | 68 +++++++++++++++++++++++++++++
 5 files changed, 138 insertions(+), 3 deletions(-)
 create mode 100644 novaclient/tests/v3/test_volumes.py
 create mode 100644 novaclient/v3/volumes.py

diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py
index 4291b13cb..772141411 100644
--- a/novaclient/tests/v3/fakes.py
+++ b/novaclient/tests/v3/fakes.py
@@ -188,7 +188,10 @@ class FakeHTTPClient(fakes_v1_1.FakeHTTPClient):
             'reset_state': ['state'],
             'create_image': ['name', 'metadata'],
             'migrate_live': ['host', 'block_migration', 'disk_over_commit'],
-            'create_backup': ['name', 'backup_type', 'rotation']}
+            'create_backup': ['name', 'backup_type', 'rotation'],
+            'attach': ['volume_id', 'device'],
+            'detach': ['volume_id'],
+            'swap_volume_attachment': ['old_volume_id', 'new_volume_id']}
 
         assert len(body.keys()) == 1
         action = list(body)[0]
diff --git a/novaclient/tests/v3/test_volumes.py b/novaclient/tests/v3/test_volumes.py
new file mode 100644
index 000000000..82f6d54a0
--- /dev/null
+++ b/novaclient/tests/v3/test_volumes.py
@@ -0,0 +1,47 @@
+# Copyright 2013 IBM Corp.
+# 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 novaclient.tests import utils
+from novaclient.tests.v3 import fakes
+
+
+class VolumesTest(utils.TestCase):
+    def setUp(self):
+        super(VolumesTest, self).setUp()
+        self.cs = self._get_fake_client()
+
+    def _get_fake_client(self):
+        return fakes.FakeClient()
+
+    def test_attach_server_volume(self):
+        v = self.cs.volumes.attach_server_volume(
+            server=1234,
+            volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983',
+            device='/dev/vdb'
+        )
+        self.cs.assert_called('POST', '/servers/1234/action')
+
+    def test_update_server_volume(self):
+        vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983'
+        v = self.cs.volumes.update_server_volume(
+            server=1234,
+            old_volume_id='Work',
+            new_volume_id=vol_id
+        )
+        self.cs.assert_called('POST', '/servers/1234/action')
+
+    def test_delete_server_volume(self):
+        self.cs.volumes.delete_server_volume(1234, 'Work')
+        self.cs.assert_called('POST', '/servers/1234/action')
diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py
index 0d8e8c3f7..07641d181 100644
--- a/novaclient/v3/client.py
+++ b/novaclient/v3/client.py
@@ -30,6 +30,7 @@ from novaclient.v3 import quotas
 from novaclient.v3 import servers
 from novaclient.v3 import services
 from novaclient.v3 import usage
+from novaclient.v3 import volumes
 
 
 class Client(object):
@@ -80,6 +81,7 @@ class Client(object):
         self.servers = servers.ServerManager(self)
         self.services = services.ServiceManager(self)
         self.usage = usage.UsageManager(self)
+        self.volumes = volumes.VolumeManager(self)
 
         # Add in any extensions...
         if extensions:
diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py
index b183860b4..2516086ed 100644
--- a/novaclient/v3/shell.py
+++ b/novaclient/v3/shell.py
@@ -1547,10 +1547,25 @@ def do_volume_attach(cs, args):
     if args.device == 'auto':
         args.device = None
 
-    volume = cs.volumes.create_server_volume(_find_server(cs, args.server).id,
+    volume = cs.volumes.attach_server_volume(_find_server(cs, args.server).id,
                                              args.volume,
                                              args.device)
-    _print_volume(volume)
+
+
+@utils.arg('server',
+    metavar='<server>',
+    help='Name or ID of server.')
+@utils.arg('attachment_id',
+    metavar='<volume>',
+    help='Attachment ID of the volume.')
+@utils.arg('new_volume',
+    metavar='<volume>',
+    help='ID of the volume to attach.')
+def do_volume_update(cs, args):
+    """Update volume attachment."""
+    volume = cs.volumes.update_server_volume(_find_server(cs, args.server).id,
+                                             args.attachment_id,
+                                             args.new_volume)
 
 
 @utils.arg('server',
diff --git a/novaclient/v3/volumes.py b/novaclient/v3/volumes.py
new file mode 100644
index 000000000..ec061a319
--- /dev/null
+++ b/novaclient/v3/volumes.py
@@ -0,0 +1,68 @@
+# Copyright 2013 IBM Corp.
+#
+#    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.
+
+"""
+Volume interface
+"""
+
+from novaclient import base
+
+
+class VolumeManager(base.Manager):
+    """
+    Manage :class:`Volume` resources.
+    """
+
+    def attach_server_volume(self, server, volume_id, device):
+        """
+        Attach a volume identified by the volume ID to the given server ID
+
+        :param server: The server (or it's ID)
+        :param volume_id: The ID of the volume to attach.
+        :param device: The device name
+        :rtype: :class:`Volume`
+        """
+        body = {'volume_id': volume_id, 'device': device}
+        return self._action('attach', server, body)
+
+    def update_server_volume(self, server, old_volume_id, new_volume_id):
+        """
+        Update the volume identified by the attachment ID, that is attached to
+        the given server ID
+
+        :param server_id: The server (or it's ID)
+        :param old_volume_id: The ID of the attachment
+        :param new_volume_id: The ID of the new volume to attach
+        :rtype: :class:`Volume`
+        """
+        body = {'new_volume_id': new_volume_id, 'old_volume_id': old_volume_id}
+        return self._action('swap_volume_attachment', server, body)
+
+    def delete_server_volume(self, server, volume_id):
+        """
+        Detach a volume identified by the attachment ID from the given server
+
+        :param server_id: The ID of the server
+        :param volume_id: The ID of the attachment
+        """
+        return self._action('detach', server, {'volume_id': volume_id})
+
+    def _action(self, action, server, info=None, **kwargs):
+        """
+        Perform a server "action" -- reboot/rebuild/resize/etc.
+        """
+        body = {action: info}
+        self.run_hooks('modify_body_for_action', body, **kwargs)
+        url = '/servers/%s/action' % base.getid(server)
+        return self.api.client.post(url, body=body)