HDDS-4864. Add acceptance tests to certify Ozone with boto3 python client. (#1976)

* HDDS-4864. Add acceptance tests to certify Ozone with boto3 python client (ghuangups via swagle)

Authored-by: Jun Huang <junhuang@Juns-MacBook-Pro.local>
diff --git a/hadoop-hdds/docs/content/recipe/BotoClient.md b/hadoop-hdds/docs/content/recipe/BotoClient.md
new file mode 100644
index 0000000..d8dc02d
--- /dev/null
+++ b/hadoop-hdds/docs/content/recipe/BotoClient.md
@@ -0,0 +1,189 @@
+---
+title: Access Ozone object store with Amazon Boto3 client
+linktitle: Ozone with Boto3 Client
+summary: How to access Ozone object store with Boto3 client?
+---
+<!---
+  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.
+-->
+
+This recipe shows how Ozone object store can be accessed from Boto3 client. Following apis were verified:
+
+ - Create bucket
+ - List bucket
+ - Head bucket
+ - Delete bucket
+ - Upload file
+ - Download file
+ - Delete objects(keys)
+ - Head object
+ - Multipart upload
+
+
+## Requirements
+
+You will need a higher version of Python3 for your Boto3 client as Boto3 installation requirement indicates at here:
+https://boto3.amazonaws.com/v1/documentation/api/latest/index.html
+
+## Obtain resource to Ozone
+You may reference Amazon Boto3 documentation regarding the creation of 's3' resources at here:
+https://boto3.amazonaws.com/v1/documentation/api/latest/guide/resources.html
+
+    s3 = boto3.resource('s3',
+                        endpoint_url='http://localhost:9878',
+                        aws_access_key_id='testuser/scm@EXAMPLE.COM',
+                        aws_secret_access_key='c261b6ecabf7d37d5f9ded654b1c724adac9bd9f13e247a235e567e8296d2999'
+    )    
+    'endpoint_url' is pointing to Ozone s3 endpoint.
+
+
+## Obtain client to Ozone via session
+You may reference Amazon Boto3 documentation regarding session at here:
+https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html
+
+    Create a session
+        session = boto3.session.Session()
+
+    Obtain s3 client to Ozone via session:
+
+        s3_client = session.client(
+            service_name='s3',
+            aws_access_key_id='testuser/scm@EXAMPLE.COM',
+            aws_secret_access_key='c261b6ecabf7d37d5f9ded654b1c724adac9bd9f13e247a235e567e8296d2999',
+            endpoint_url='http://localhost:9878',
+        )
+        'endpoint_url' is pointing to Ozone s3 endpoint.
+
+    In our code sample below, we're demonstrating the usage of both s3 and s3_client.
+
+There are multiple ways to configure Boto3 client credentials if you're connecting to a secured cluster. In these cases, 
+the above lines of passing 'aws_access_key_id' and 'aws_secret_access_key' when creating Ozone s3 client shall be skipped.
+
+Please refer to Boto3 documentation for details at here:
+https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html
+
+
+### Create a bucket
+    response = s3_client.create_bucket(Bucket='bucket1')
+    print(response)
+
+This will create a bucket 'bucket1' in Ozone volume 's3v'.
+
+### List buckets
+    response = s3_client.list_buckets()
+    print('Existing buckets:')
+    for bucket in response['Buckets']:
+        print(f'  {bucket["Name"]}')
+
+This will list all buckets in Ozone volume 's3v'.
+
+### Head a bucket
+    response = s3_client.head_bucket(Bucket='bucket1')
+    print(response)
+
+This will head bucket 'bucket1' in Ozone volume 's3v'.
+
+### Delete a bucket
+    response = s3_client.delete_bucket(Bucket='bucket1')
+    print(response)
+
+This will delete the bucket 'bucket1' from Ozone volume 's3v'.
+
+### Upload a file
+    response = s3.Bucket('bucket1').upload_file('./README.md','README.md')
+    print(response)
+
+This will upload 'README.md' to Ozone creates a key 'README.md' in volume 's3v'.
+
+### Download a file
+    response = s3.Bucket('bucket1').download_file('README.md', 'download.md')
+    print(response)
+
+This will download 'README.md' from Ozone volume 's3v' to local and create a file with name 'download.md'.
+
+### Head an object
+    response = s3_client.head_object(Bucket='bucket1', Key='README.md')
+    print(response)
+
+This will head object 'README.md' from Ozone volume 's3v' in the bucket 'bucket1'.
+
+### Delete Objects
+    response = s3_client.delete_objects(
+        Bucket='bucket1',
+        Delete={
+            'Objects': [
+                {
+                    'Key': 'README4.md',
+                },
+                {
+                    'Key': 'README3.md',
+                },
+            ],
+            'Quiet': False,
+        },
+    )
+
+This will delete objects 'README3.md' and 'README4.md' from Ozone volume 's3v' in bucket 'bucket1'.
+
+### Multipart upload
+    response = s3_client.create_multipart_upload(Bucket='bucket1', Key='key1')
+    print(response)
+    uid=response['UploadId']
+    print(uid)
+
+    response = s3_client.upload_part_copy(
+        Bucket='bucket1',
+        CopySource='/bucket1/maven.gz',
+        Key='key1',
+        PartNumber=1,
+        UploadId=str(uid)
+    )
+    print(response)
+    etag1=response.get('CopyPartResult').get('ETag')
+    print(etag1)
+
+    response = s3_client.upload_part_copy(
+        Bucket='bucket1',
+        CopySource='/bucket1/maven1.gz',
+        Key='key1',
+        PartNumber=2,
+        UploadId=str(uid)
+    )
+    print(response)
+    etag2=response.get('CopyPartResult').get('ETag')
+    print(etag2)
+
+    response = s3_client.complete_multipart_upload(
+        Bucket='bucket1',
+        Key='key1',
+        MultipartUpload={
+            'Parts': [
+                {
+                    'ETag': str(etag1),
+                    'PartNumber': 1,
+                },
+                {
+                    'ETag': str(etag2),
+                    'PartNumber': 2,
+                },
+            ],
+        },
+        UploadId=str(uid),
+    )
+    print(response)
+
+This will use 'maven.gz' and 'maven1.gz' as copy source from Ozone volume 's3v' to create a new object 'key1'
+in Ozone volume 's3v'. Please note 'ETag's is required and important for the call.
diff --git a/hadoop-ozone/dist/pom.xml b/hadoop-ozone/dist/pom.xml
index c1da927..5454799 100644
--- a/hadoop-ozone/dist/pom.xml
+++ b/hadoop-ozone/dist/pom.xml
@@ -28,7 +28,7 @@
   <properties>
     <file.encoding>UTF-8</file.encoding>
     <downloadSources>true</downloadSources>
-    <docker.ozone-runner.version>20200625-1</docker.ozone-runner.version>
+    <docker.ozone-runner.version>20210226-1</docker.ozone-runner.version>
   </properties>
 
   <build>
diff --git a/hadoop-ozone/dist/src/main/smoketest/s3/boto3.robot b/hadoop-ozone/dist/src/main/smoketest/s3/boto3.robot
new file mode 100644
index 0000000..6a575f3
--- /dev/null
+++ b/hadoop-ozone/dist/src/main/smoketest/s3/boto3.robot
@@ -0,0 +1,34 @@
+# 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.
+
+*** Settings ***
+Documentation       S3 gateway test with Boto3 Client
+Library             OperatingSystem
+Library             String
+Library             Process
+Library             BuiltIn
+Resource            ../commonlib.robot
+Resource            ./commonawslib.robot
+Test Timeout        15 minutes
+Suite Setup         Setup s3 tests
+
+*** Variables ***
+${ENDPOINT_URL}       http://s3g:9878
+${BUCKET}             generated
+
+*** Test Cases ***
+
+Bobo3 Client Test
+    ${result} =    Execute    python3 /opt/hadoop/smoketest/s3/boto_client.py ${ENDPOINT_URL} ${BUCKET}
diff --git a/hadoop-ozone/dist/src/main/smoketest/s3/boto_client.py b/hadoop-ozone/dist/src/main/smoketest/s3/boto_client.py
new file mode 100755
index 0000000..5185271
--- /dev/null
+++ b/hadoop-ozone/dist/src/main/smoketest/s3/boto_client.py
@@ -0,0 +1,264 @@
+# 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 os
+import sys
+import random
+import string
+import logging
+import json
+import unittest
+import boto3
+from botocore.client import Config
+from botocore.exceptions import ClientError
+import os.path
+from os import path
+
+class TestBotoClient(unittest.TestCase):
+
+    s3 = None
+    s3_client = None
+    setup_done = False
+    target_bucket = None
+    ozone_endpoint_url = None
+ 
+    def setUp(self):
+        if TestBotoClient.setup_done:
+            return
+
+        TestBotoClient.ozone_endpoint_url = sys.argv[1]
+        TestBotoClient.target_bucket = sys.argv[2]
+        TestBotoClient.setup_done = True
+
+        TestBotoClient.s3 = boto3.resource('s3',
+            endpoint_url=self.ozone_endpoint_url
+        )
+
+        TestBotoClient.s3_client = boto3.session.Session().client(
+            service_name='s3',
+            endpoint_url=self.ozone_endpoint_url
+        )
+
+        try:
+            response = self.s3_client.create_bucket(Bucket='boto-bucket999')
+            print(response)
+
+            response = self.s3_client.upload_file("README.md", str(self.target_bucket), "README3.md")
+            print(response)
+
+            response = self.s3.Bucket(str(self.target_bucket)).upload_file('README.md','README4.md')
+            print(response)
+
+            self.s3.Bucket(str(self.target_bucket)).upload_file('README.md','README10.md')
+            print(response)
+        except ClientError as e:
+            logging.error(e)
+            print(e)
+
+        f = open('multiUpload.gz',"wb")
+        f.seek(10485760)
+        f.write(b"\0")
+        f.close()
+        self.s3.Bucket(str(self.target_bucket)).upload_file('./multiUpload.gz','multiUpload.1.gz')
+        self.s3.Bucket(str(self.target_bucket)).upload_file('./multiUpload.gz','multiUpload.2.gz')
+
+    def test_create_bucket(self):
+        self.assertTrue(self.s3_client is not None)
+        try:
+           letters = string.ascii_lowercase
+           bucket_name = ''.join(random.choice(letters) for i in range(10))
+           response = self.s3_client.create_bucket(Bucket='bucket-' + str(bucket_name))
+           print(response)
+           self.assertTrue(str(bucket_name) in response.get('Location'))
+           self.assertTrue(response.get('ResponseMetadata').get('HTTPStatusCode') == 200)
+        except ClientError as e:
+            print(e)
+            logging.error(e)
+            return False
+        return True
+
+    def test_list_bucket(self):
+        self.assertTrue(self.s3_client is not None)
+        try:
+           response = self.s3_client.list_buckets()
+           self.assertTrue(response.get('ResponseMetadata').get('HTTPStatusCode') == 200)
+           print(response)           
+        except ClientError as e:
+            print(e)
+            logging.error(e)
+            return False
+        return True
+
+    def test_head_bucket(self):
+        self.assertTrue(self.s3_client is not None)
+        try:
+           response = self.s3_client.head_bucket(Bucket=self.target_bucket)
+           self.assertTrue(response.get('ResponseMetadata').get('HTTPStatusCode') == 200)
+           print(response)
+        except ClientError as e:
+            print(e)
+            logging.error(e)
+            return False
+        return True
+
+    def test_bucket_delete(self):
+        self.assertTrue(self.s3_client is not None)
+        try:
+           response = self.s3_client.delete_bucket(Bucket='boto-bucket999')
+           self.assertTrue(response.get('ResponseMetadata').get('HTTPStatusCode') == 204)
+           print(response)
+        except ClientError as e:
+            logging.error(e)
+            return False
+        return True
+
+    def test_upload_file(self):
+        self.assertTrue(self.s3 is not None)
+        try:
+           self.s3.Bucket(str(self.target_bucket)).upload_file('./README.md','README1.md')
+           response = self.s3_client.head_object(Bucket=str(self.target_bucket), Key='README1.md')
+           self.assertTrue(response.get('ResponseMetadata').get('HTTPStatusCode') == 200)
+           print(response)
+        except ClientError as e:
+            print(e)
+            logging.error(e)
+            return False
+        return True
+
+    def test_download_file(self):
+        self.assertTrue(self.s3 is not None)
+        try:
+           self.s3.Bucket(str(self.target_bucket)).download_file('README10.md', 'download.md')
+           self.assertTrue(path.exists("./download.md"))
+        except ClientError as e:
+            print(e)
+            logging.error(e)
+            return False
+        return True
+
+    def test_delete_objects(self):
+        self.assertTrue(self.s3_client is not None)
+        try:
+            response = self.s3_client.delete_objects(
+                Bucket=str(self.target_bucket),
+                Delete={
+                    'Objects': [
+                        {
+                            'Key': 'README4.md',
+                        },
+                        {
+                            'Key': 'README3.md',
+                        },
+                    ],
+                    'Quiet': False,
+                },
+            )
+            self.assertTrue(response.get('ResponseMetadata').get('HTTPStatusCode') == 200)
+            print(response)
+        except ClientError as e:
+            print(e)
+            logging.error(e)
+            return False
+        return True
+
+    def test_head_object(self):
+        self.assertTrue(self.s3_client is not None)
+        try:
+           response = self.s3_client.head_object(Bucket=str(self.target_bucket), Key='README10.md')
+           self.assertTrue(response.get('ResponseMetadata').get('HTTPStatusCode') == 200)
+           print(response)
+        except ClientError as e:
+            print(e)
+            logging.error(e)
+            return False
+        return True
+
+    def test_multi_uploads(self):
+        self.assertTrue(self.s3_client is not None)
+        try:
+            lts = string.ascii_lowercase
+            key_name = ''.join(random.choice(lts) for i in range(10))
+            response = self.s3_client.create_multipart_upload(Bucket=str(self.target_bucket), Key=str(key_name))
+            print(response)
+            uid=response['UploadId']
+
+            copy1 = self.target_bucket + "/multiUpload.1.gz"
+            response = self.s3_client.upload_part_copy(
+                Bucket=str(self.target_bucket),
+                CopySource=str(copy1),
+                Key=str(key_name),
+                PartNumber=1,
+                UploadId=str(uid)
+            )
+            etag1=response.get('CopyPartResult').get('ETag')
+
+            copy2 = self.target_bucket + "/multiUpload.2.gz"
+            response = self.s3_client.upload_part_copy(
+                Bucket=str(self.target_bucket),
+                CopySource=str(copy2),
+                Key=str(key_name),
+                PartNumber=2,
+                UploadId=str(uid)
+            )
+            etag2=response.get('CopyPartResult').get('ETag')
+     
+            response = self.s3_client.complete_multipart_upload(
+                Bucket=str(self.target_bucket),
+                Key=str(key_name),
+                MultipartUpload={
+                    'Parts': [
+                        {
+                            'ETag': str(etag1),
+                            'PartNumber': 1,
+                        },
+                        {
+                            'ETag': str(etag2),
+                            'PartNumber': 2,
+                        },
+                    ],
+                },
+                UploadId=str(uid),
+            )
+            self.assertTrue(response.get('ResponseMetadata').get('HTTPStatusCode') == 200)
+            print(response)
+        except ClientError as e:
+            print(e)
+            logging.error(e)
+            return False
+        return True
+
+if __name__ == '__main__':
+    #unittest.main()
+    suite = unittest.TestSuite()
+
+    suite.addTest(TestBotoClient('test_create_bucket'))
+    suite.addTest(TestBotoClient('test_list_bucket'))
+    suite.addTest(TestBotoClient('test_head_bucket'))
+    suite.addTest(TestBotoClient('test_bucket_delete'))
+    suite.addTest(TestBotoClient('test_upload_file'))
+    suite.addTest(TestBotoClient('test_download_file'))
+    suite.addTest(TestBotoClient('test_delete_objects'))
+    suite.addTest(TestBotoClient('test_head_object'))
+    suite.addTest(TestBotoClient('test_multi_uploads'))
+
+    result = unittest.TextTestRunner(verbosity=2).run(suite)
+ 
+    if result.wasSuccessful():
+        print("Boto3 Client Test PASSED!")
+        exit(0)
+    else:
+        print("Boto3 Client Test FAILED!")
+        exit(1)