import logging import os import uuid from time import sleep from typing import Optional import allure import pytest import urllib3 from botocore.exceptions import ClientError from pytest_tests.helpers.aws_cli_client import AwsCliClient from pytest_tests.helpers.cli_helpers import log_command_execution from pytest_tests.steps.s3_gate_bucket import S3_SYNC_WAIT_TIME ########################################################## # Disabling warnings on self-signed certificate which the # boto library produces on requests to S3-gate in dev-env. urllib3.disable_warnings() ########################################################## logger = logging.getLogger("NeoLogger") ACL_COPY = [ "private", "public-read", "public-read-write", "authenticated-read", "aws-exec-read", "bucket-owner-read", "bucket-owner-full-control", ] ASSETS_DIR = os.getenv("ASSETS_DIR", "TemporaryDir/") @allure.step("List objects S3 v2") def list_objects_s3_v2(s3_client, bucket: str, full_output: bool = False) -> list: try: response = s3_client.list_objects_v2(Bucket=bucket) content = response.get("Contents", []) log_command_execution("S3 v2 List objects result", response) obj_list = [] for obj in content: obj_list.append(obj["Key"]) logger.info(f"Found s3 objects: {obj_list}") return response if full_output else obj_list except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("List objects S3") def list_objects_s3(s3_client, bucket: str, full_output: bool = False) -> list: try: response = s3_client.list_objects(Bucket=bucket) content = response.get("Contents", []) log_command_execution("S3 List objects result", response) obj_list = [] for obj in content: obj_list.append(obj["Key"]) logger.info(f"Found s3 objects: {obj_list}") return response if full_output else obj_list except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("List objects versions S3") def list_objects_versions_s3(s3_client, bucket: str, full_output: bool = False) -> list: try: response = s3_client.list_object_versions(Bucket=bucket) versions = response.get("Versions", []) log_command_execution("S3 List objects versions result", response) return response if full_output else versions except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("List objects delete markers S3") def list_objects_delete_markers_s3(s3_client, bucket: str, full_output: bool = False) -> list: try: response = s3_client.list_object_versions(Bucket=bucket) delete_markers = response.get("DeleteMarkers", []) log_command_execution("S3 List objects delete markers result", response) return response if full_output else delete_markers except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Put object S3") def put_object_s3(s3_client, bucket: str, filepath: str, **kwargs): filename = os.path.basename(filepath) if isinstance(s3_client, AwsCliClient): file_content = filepath else: with open(filepath, "rb") as put_file: file_content = put_file.read() try: params = {"Body": file_content, "Bucket": bucket, "Key": filename} if kwargs: params = {**params, **kwargs} response = s3_client.put_object(**params) log_command_execution("S3 Put object result", response) return response.get("VersionId") except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Head object S3") def head_object_s3(s3_client, bucket: str, object_key: str, version_id: Optional[str] = None): try: params = {"Bucket": bucket, "Key": object_key} if version_id: params["VersionId"] = version_id response = s3_client.head_object(**params) log_command_execution("S3 Head object result", response) return response except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Delete object S3") def delete_object_s3( s3_client, bucket: str, object_key: str, version_id: Optional[str] = None ) -> dict: try: params = {"Bucket": bucket, "Key": object_key} if version_id: params["VersionId"] = version_id response = s3_client.delete_object(**params) log_command_execution("S3 Delete object result", response) sleep(S3_SYNC_WAIT_TIME) return response except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Delete objects S3") def delete_objects_s3(s3_client, bucket: str, object_keys: list): try: response = s3_client.delete_objects(Bucket=bucket, Delete=_make_objs_dict(object_keys)) log_command_execution("S3 Delete objects result", response) sleep(S3_SYNC_WAIT_TIME) assert ( "Errors" not in response ), f'The following objects have not been deleted: {[err_info["Key"] for err_info in response["Errors"]]}.\nError Message: {response["Errors"]["Message"]}' return response except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Delete object versions S3") def delete_object_versions_s3(s3_client, bucket: str, object_versions: list): try: # Build deletion list in S3 format delete_list = { "Objects": [ { "Key": object_version["Key"], "VersionId": object_version["VersionId"], } for object_version in object_versions ] } response = s3_client.delete_objects(Bucket=bucket, Delete=delete_list) log_command_execution("S3 Delete objects result", response) return response except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Delete object versions S3 without delete markers") def delete_object_versions_s3_without_dm(s3_client, bucket: str, object_versions: list): try: # Delete objects without creating delete markers for object_version in object_versions: params = { "Bucket": bucket, "Key": object_version["Key"], "VersionId": object_version["VersionId"], } response = s3_client.delete_object(**params) log_command_execution("S3 Delete object result", response) return response except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Put object ACL") def put_object_acl_s3( s3_client, bucket: str, object_key: str, acl: Optional[str] = None, grant_write: Optional[str] = None, grant_read: Optional[str] = None, ) -> list: if not isinstance(s3_client, AwsCliClient): pytest.skip("Method put_object_acl is not supported by boto3 client") params = {"Bucket": bucket, "Key": object_key} if acl: params.update({"ACL": acl}) elif grant_write or grant_read: if grant_write: params.update({"GrantWrite": grant_write}) elif grant_read: params.update({"GrantRead": grant_read}) try: response = s3_client.put_object_acl(**params) log_command_execution("S3 ACL objects result", response) return response.get("Grants") except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Get object ACL") def get_object_acl_s3( s3_client, bucket: str, object_key: str, version_id: Optional[str] = None ) -> list: params = {"Bucket": bucket, "Key": object_key} try: if version_id: params.update({"VersionId": version_id}) response = s3_client.get_object_acl(**params) log_command_execution("S3 ACL objects result", response) return response.get("Grants") except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Copy object S3") def copy_object_s3( s3_client, bucket: str, object_key: str, bucket_dst: Optional[str] = None, **kwargs ) -> str: filename = os.path.join(os.getcwd(), str(uuid.uuid4())) try: params = { "Bucket": bucket_dst or bucket, "CopySource": f"{bucket}/{object_key}", "Key": filename, } if "ACL" in kwargs and kwargs["ACL"] in ACL_COPY: params.update({"ACL": kwargs["ACL"]}) if "metadata_directive" in kwargs.keys(): params.update({"MetadataDirective": kwargs["metadata_directive"]}) if "metadata_directive" in kwargs.keys() and "metadata" in kwargs.keys(): params.update({"Metadata": kwargs["metadata"]}) if "tagging_directive" in kwargs.keys(): params.update({"TaggingDirective": kwargs["tagging_directive"]}) if "tagging_directive" in kwargs.keys() and "tagging" in kwargs.keys(): params.update({"Tagging": kwargs["tagging"]}) response = s3_client.copy_object(**params) log_command_execution("S3 Copy objects result", response) return filename except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Get object S3") def get_object_s3( s3_client, bucket: str, object_key: str, version_id: Optional[str] = None, range: Optional[list] = None, full_output: bool = False, ): filename = os.path.join(os.getcwd(), ASSETS_DIR, str(uuid.uuid4())) try: params = {"Bucket": bucket, "Key": object_key} if version_id: params["VersionId"] = version_id if isinstance(s3_client, AwsCliClient): params["file_path"] = filename if range: params["Range"] = f"bytes={range[0]}-{range[1]}" response = s3_client.get_object(**params) log_command_execution("S3 Get objects result", response) if not isinstance(s3_client, AwsCliClient): with open(f"{filename}", "wb") as get_file: chunk = response["Body"].read(1024) while chunk: get_file.write(chunk) chunk = response["Body"].read(1024) return response if full_output else filename except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Create multipart upload S3") def create_multipart_upload_s3(s3_client, bucket_name: str, object_key: str) -> str: try: response = s3_client.create_multipart_upload(Bucket=bucket_name, Key=object_key) log_command_execution("S3 Created multipart upload", response) assert response.get("UploadId"), f"Expected UploadId in response:\n{response}" return response.get("UploadId") except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("List multipart uploads S3") def list_multipart_uploads_s3(s3_client, bucket_name: str) -> Optional[list[dict]]: try: response = s3_client.list_multipart_uploads(Bucket=bucket_name) log_command_execution("S3 List multipart upload", response) return response.get("Uploads") except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Abort multipart upload S3") def abort_multipart_upload_s3(s3_client, bucket_name: str, object_key: str, upload_id: str): try: response = s3_client.abort_multipart_upload( Bucket=bucket_name, Key=object_key, UploadId=upload_id ) log_command_execution("S3 Abort multipart upload", response) except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Upload part S3") def upload_part_s3( s3_client, bucket_name: str, object_key: str, upload_id: str, part_num: int, filepath: str ) -> str: if isinstance(s3_client, AwsCliClient): file_content = filepath else: with open(filepath, "rb") as put_file: file_content = put_file.read() try: response = s3_client.upload_part( UploadId=upload_id, Bucket=bucket_name, Key=object_key, PartNumber=part_num, Body=file_content, ) log_command_execution("S3 Upload part", response) assert response.get("ETag"), f"Expected ETag in response:\n{response}" return response.get("ETag") except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Upload copy part S3") def upload_part_copy_s3( s3_client, bucket_name: str, object_key: str, upload_id: str, part_num: int, copy_source: str ) -> str: try: response = s3_client.upload_part_copy( UploadId=upload_id, Bucket=bucket_name, Key=object_key, PartNumber=part_num, CopySource=copy_source, ) log_command_execution("S3 Upload copy part", response) assert response.get("CopyPartResult").get("ETag"), f"Expected ETag in response:\n{response}" return response.get("CopyPartResult").get("ETag") except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("List parts S3") def list_parts_s3(s3_client, bucket_name: str, object_key: str, upload_id: str) -> list[dict]: try: response = s3_client.list_parts(UploadId=upload_id, Bucket=bucket_name, Key=object_key) log_command_execution("S3 List part", response) assert response.get("Parts"), f"Expected Parts in response:\n{response}" return response.get("Parts") except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Complete multipart upload S3") def complete_multipart_upload_s3( s3_client, bucket_name: str, object_key: str, upload_id: str, parts: list ): try: parts = [{"ETag": etag, "PartNumber": part_num} for part_num, etag in parts] response = s3_client.complete_multipart_upload( Bucket=bucket_name, Key=object_key, UploadId=upload_id, MultipartUpload={"Parts": parts} ) log_command_execution("S3 Complete multipart upload", response) except ClientError as err: raise Exception( f'Error Message: {err.response["Error"]["Message"]}\n' f'Http status code: {err.response["ResponseMetadata"]["HTTPStatusCode"]}' ) from err @allure.step("Put object retention") def put_object_retention( s3_client, bucket_name: str, object_key: str, retention: dict, version_id: Optional[str] = None, bypass_governance_retention: Optional[bool] = None, ): try: params = {"Bucket": bucket_name, "Key": object_key, "Retention": retention} if version_id: params.update({"VersionId": version_id}) if not bypass_governance_retention is None: params.update({"BypassGovernanceRetention": bypass_governance_retention}) s3_client.put_object_retention(**params) log_command_execution("S3 Put object retention ", str(retention)) except ClientError as err: raise Exception(f"Got error during put object tagging: {err}") from err @allure.step("Put object legal hold") def put_object_legal_hold( s3_client, bucket_name: str, object_key: str, legal_hold: str, version_id: Optional[str] = None ): try: params = {"Bucket": bucket_name, "Key": object_key, "LegalHold": {"Status": legal_hold}} if version_id: params.update({"VersionId": version_id}) s3_client.put_object_legal_hold(**params) log_command_execution("S3 Put object legal hold ", str(legal_hold)) except ClientError as err: raise Exception(f"Got error during put object tagging: {err}") from err @allure.step("Put object tagging") def put_object_tagging(s3_client, bucket_name: str, object_key: str, tags: list): try: tags = [{"Key": tag_key, "Value": tag_value} for tag_key, tag_value in tags] tagging = {"TagSet": tags} s3_client.put_object_tagging(Bucket=bucket_name, Key=object_key, Tagging=tagging) log_command_execution("S3 Put object tagging", str(tags)) except ClientError as err: raise Exception(f"Got error during put object tagging: {err}") from err @allure.step("Get object tagging") def get_object_tagging( s3_client, bucket_name: str, object_key: str, version_id: Optional[str] = None ) -> list: try: params = {"Bucket": bucket_name, "Key": object_key} if version_id: params.update({"VersionId": version_id}) response = s3_client.get_object_tagging(**params) log_command_execution("S3 Get object tagging", response) return response.get("TagSet") except ClientError as err: raise Exception(f"Got error during get object tagging: {err}") from err @allure.step("Delete object tagging") def delete_object_tagging(s3_client, bucket_name: str, object_key: str): try: response = s3_client.delete_object_tagging(Bucket=bucket_name, Key=object_key) log_command_execution("S3 Delete object tagging", response) except ClientError as err: raise Exception(f"Got error during delete object tagging: {err}") from err @allure.step("Get object attributes") def get_object_attributes( s3_client, bucket_name: str, object_key: str, *attributes: str, version_id: Optional[str] = None, max_parts: Optional[int] = None, part_number: Optional[int] = None, get_full_resp: bool = True, ) -> dict: try: if not isinstance(s3_client, AwsCliClient): logger.warning("Method get_object_attributes is not supported by boto3 client") return {} response = s3_client.get_object_attributes( bucket_name, object_key, *attributes, version_id=version_id, max_parts=max_parts, part_number=part_number, ) log_command_execution("S3 Get object attributes", response) for attr in attributes: assert attr in response, f"Expected attribute {attr} in {response}" if get_full_resp: return response else: return response.get(attributes[0]) except ClientError as err: raise Exception(f"Got error during get object attributes: {err}") from err def _make_objs_dict(key_names): objs_list = [] for key in key_names: obj_dict = {"Key": key} objs_list.append(obj_dict) objs_dict = {"Objects": objs_list} return objs_dict