Merge pull request #1340 from CatalystCode/azure-blobs-api-version-update

Update Azure Blob Storage API version to 2016-05-31
diff --git a/CHANGES.rst b/CHANGES.rst
index 3f0ae24..1eccfc1 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -187,11 +187,16 @@
   [Clemens Wolff - @c-w]
 
 - [Azure Blobs] Fix a bug with Azure storage driver works when used against a
-  storage account that was created using ``kind=BlobStrage``. The includes
-  updating minimum API version used / supported by storage driver from
-  ``2012-02-12`` to ``2014-02-14'``. (LIBCLOUD-851, GITHUB-1202, GITHUB-1294)
+  storage account that was created using ``kind=BlobStrage``. This includes
+  updating the minimum API version used / supported by the storage driver from
+  ``2012-02-12`` to ``2014-02-14``. (LIBCLOUD-851, GITHUB-1202, GITHUB-1294)
   [Clemens Wolff - @c-w, Davis Kirkendall - @daviskirk]
 
+- [Azure Blobs] Increase the maximum size of block blobs that can be created
+  to 100 MB. This includes updating the minimum API version used / supported
+  by the storage driver from ``2014-02-14`` to ``2016-05-31``. (GITHUB-1340)
+  [Clemens Wolff - @c-w]
+
 - [Azure Blobs] Set the minimum required version of requests to ``2.5.0`` since
   requests ``2.4.0`` and earlier exhibit XML parsing errors of Azure Storage
   responses. (GITHUB-1325, GITHUB-1322)
diff --git a/libcloud/common/azure.py b/libcloud/common/azure.py
index ff81acf..b4bfb9c 100644
--- a/libcloud/common/azure.py
+++ b/libcloud/common/azure.py
@@ -159,22 +159,8 @@
             CanonicalizedHeaders +
             CanonicalizedResource;
         """
-        special_header_values = []
         xms_header_values = []
         param_list = []
-        special_header_keys = [
-            'content-encoding',
-            'content-language',
-            'content-length',
-            'content-md5',
-            'content-type',
-            'date',
-            'if-modified-since',
-            'if-match',
-            'if-none-match',
-            'if-unmodified-since',
-            'range'
-        ]
 
         # Split the x-ms headers and normal headers and make everything
         # lower case
@@ -188,16 +174,8 @@
                 headers_copy[header] = value
 
         # Get the values for the headers in the specific order
-        for header in special_header_keys:
-            header = header.lower()  # Just for safety
-            if header in headers_copy:
-                special_header_values.append(headers_copy[header])
-            elif header == "content-length" and method not in ("GET", "HEAD"):
-                # Must be '0' unless method is GET or HEAD
-                # https://docs.microsoft.com/en-us/rest/api/storageservices/authentication-for-the-azure-storage-services
-                special_header_values.append('0')
-            else:
-                special_header_values.append('')
+        special_header_values = self._format_special_header_values(
+            headers_copy, method)
 
         # Prepare the first section of the string to be signed
         values_to_sign = [method] + special_header_values
@@ -229,6 +207,39 @@
 
         return 'SharedKey %s:%s' % (self.user_id, b64_hmac.decode('utf-8'))
 
+    def _format_special_header_values(self, headers, method):
+        is_change = method not in ('GET', 'HEAD')
+        is_old_api = self.API_VERSION <= '2014-02-14'
+
+        special_header_keys = [
+            'content-encoding',
+            'content-language',
+            'content-length',
+            'content-md5',
+            'content-type',
+            'date',
+            'if-modified-since',
+            'if-match',
+            'if-none-match',
+            'if-unmodified-since',
+            'range'
+        ]
+
+        special_header_values = []
+
+        for header in special_header_keys:
+            header = header.lower()  # Just for safety
+            if header in headers:
+                special_header_values.append(headers[header])
+            elif header == 'content-length' and is_change and is_old_api:
+                # For old API versions, the Content-Length header must be '0'
+                # https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#content-length-header-in-version-2014-02-14-and-earlier
+                special_header_values.append('0')
+            else:
+                special_header_values.append('')
+
+        return special_header_values
+
 
 class AzureBaseDriver(object):
     name = "Microsoft Azure Service Management API"
diff --git a/libcloud/storage/drivers/azure_blobs.py b/libcloud/storage/drivers/azure_blobs.py
index b7db039..72898c7 100644
--- a/libcloud/storage/drivers/azure_blobs.py
+++ b/libcloud/storage/drivers/azure_blobs.py
@@ -43,15 +43,8 @@
 RESPONSES_PER_REQUEST = 100
 
 # As per the Azure documentation, if the upload file size is less than
-# 64MB, we can upload it in a single request. However, in real life azure
-# servers seem to disconnect randomly after around 5 MB or 200s of upload.
-# So, it is better that for file sizes greater than 4MB, we upload it in
-# chunks.
-# Also, with large sizes, if we use a lease, the lease will timeout after
-# 60 seconds, but the upload might still be in progress. This can be
-# handled in code, but if we use chunked uploads, the lease renewal will
-# happen automatically.
-AZURE_BLOCK_MAX_SIZE = 4 * 1024 * 1024
+# 100MB, we can upload it in a single request.
+AZURE_BLOCK_MAX_SIZE = 100 * 1024 * 1024
 
 # Azure block blocks must be maximum 4MB
 # Azure page blobs must be aligned in 512 byte boundaries (4MB fits that)
@@ -184,9 +177,7 @@
 
         return action
 
-    # this is the minimum api version supported by storage accounts of kinds
-    # StorageV2, Storage and BlobStorage
-    API_VERSION = '2014-02-14'
+    API_VERSION = '2016-05-31'
 
 
 class AzureBlobsStorageDriver(StorageDriver):
diff --git a/libcloud/test/common/test_azure.py b/libcloud/test/common/test_azure.py
new file mode 100644
index 0000000..a945169
--- /dev/null
+++ b/libcloud/test/common/test_azure.py
@@ -0,0 +1,64 @@
+# 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 sys
+import unittest
+
+from libcloud.common.azure import AzureConnection
+from libcloud.test import LibcloudTestCase
+
+
+class AzureConnectionTestCase(LibcloudTestCase):
+    def setUp(self):
+        self.conn = AzureConnection('user', 'key')
+
+    def test_content_length_is_used_if_set(self):
+        headers = {'content-length': '123'}
+        method = 'PUT'
+
+        values = self.conn._format_special_header_values(headers, method)
+
+        self.assertEqual(values[2], '123')
+
+    def test_content_length_is_blank_if_new_api_version(self):
+        headers = {}
+        method = 'PUT'
+        self.conn.API_VERSION = '2018-11-09'
+
+        values = self.conn._format_special_header_values(headers, method)
+
+        self.assertEqual(values[2], '')
+
+    def test_content_length_is_zero_if_write_and_old_api_version(self):
+        headers = {}
+        method = 'PUT'
+        self.conn.API_VERSION = '2011-08-18'
+
+        values = self.conn._format_special_header_values(headers, method)
+
+        self.assertEqual(values[2], '0')
+
+    def test_content_length_is_blank_if_read_and_old_api_version(self):
+        headers = {}
+        method = 'GET'
+        self.conn.API_VERSION = '2011-08-18'
+
+        values = self.conn._format_special_header_values(headers, method)
+
+        self.assertEqual(values[2], '')
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())
diff --git a/libcloud/test/storage/fixtures/azure_blobs/list_objects_1.xml b/libcloud/test/storage/fixtures/azure_blobs/list_objects_1.xml
index ec140ad..9358ae4 100644
--- a/libcloud/test/storage/fixtures/azure_blobs/list_objects_1.xml
+++ b/libcloud/test/storage/fixtures/azure_blobs/list_objects_1.xml
@@ -13,9 +13,11 @@
                 <Content-Language />
                 <Content-MD5>1B2M2Y8AsgTpgAmY7PhCfg==</Content-MD5>
                 <Cache-Control />
+                <Content-Disposition />
                 <BlobType>BlockBlob</BlobType>
                 <LeaseStatus>unlocked</LeaseStatus>
                 <LeaseState>available</LeaseState>
+                <ServerEncrypted>true</ServerEncrypted>
             </Properties>
             <Metadata>
                 <meta1>value1</meta1>
@@ -33,9 +35,11 @@
                 <Content-Language />
                 <Content-MD5>ttgbNgpWctgMJ0MPORU+LA==</Content-MD5>
                 <Cache-Control />
+                <Content-Disposition />
                 <BlobType>BlockBlob</BlobType>
                 <LeaseStatus>unlocked</LeaseStatus>
                 <LeaseState>available</LeaseState>
+                <ServerEncrypted>true</ServerEncrypted>
             </Properties>
             <Metadata>
                 <meta1>value1</meta1>
diff --git a/libcloud/test/storage/fixtures/azure_blobs/list_objects_2.xml b/libcloud/test/storage/fixtures/azure_blobs/list_objects_2.xml
index d71e044..c3afdd4 100644
--- a/libcloud/test/storage/fixtures/azure_blobs/list_objects_2.xml
+++ b/libcloud/test/storage/fixtures/azure_blobs/list_objects_2.xml
@@ -14,9 +14,11 @@
                 <Content-Language />
                 <Content-MD5>ttgbNgpWctgMJ0MPORU+LA==</Content-MD5>
                 <Cache-Control />
+                <Content-Disposition />
                 <BlobType>BlockBlob</BlobType>
                 <LeaseStatus>unlocked</LeaseStatus>
                 <LeaseState>available</LeaseState>
+                <ServerEncrypted>true</ServerEncrypted>
             </Properties>
             <Metadata />
         </Blob>
@@ -30,9 +32,11 @@
                 <Content-Encoding /><Content-Language />
                 <Content-MD5>1B2M2Y8AsgTpgAmY7PhCfg==</Content-MD5>
                 <Cache-Control />
+                <Content-Disposition />
                 <BlobType>BlockBlob</BlobType>
                 <LeaseStatus>unlocked</LeaseStatus>
                 <LeaseState>available</LeaseState>
+                <ServerEncrypted>true</ServerEncrypted>
             </Properties>
             <Metadata />
         </Blob>