Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cosalib: create libs for s3 and ec2 operations #840

Merged
merged 1 commit into from
Oct 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 5 additions & 20 deletions src/cmd-buildprep
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ import os
import subprocess
import sys
import requests
import boto3
import shutil
from botocore.exceptions import ClientError
from tenacity import retry, retry_if_exception_type

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from cosalib.builds import Builds, BUILDFILES
from cosalib.cmdlib import load_json, rm_allow_noent, retry_stop, retry_s3_exception, retry_callback # noqa: E402
from cosalib.cmdlib import load_json, rm_allow_noent, retry_stop, retry_callback
from cosalib.s3 import download_file, head_bucket, head_object

retry_requests_exception = (retry_if_exception_type(requests.Timeout) |
retry_if_exception_type(requests.ReadTimeout) |
Expand Down Expand Up @@ -167,33 +166,19 @@ class HTTPFetcher(Fetcher):

class S3Fetcher(Fetcher):

def __init__(self, url_base):
super().__init__(url_base)
self.s3 = boto3.client('s3')
self.s3config = boto3.s3.transfer.TransferConfig(
num_download_attempts=5
)

def fetch_impl(self, url, dest):
assert url.startswith("s3://")
bucket, key = url[len("s3://"):].split('/', 1)
# this function does not need to be retried with the decorator as download_file would
# retry automatically based on s3config settings
self.s3.download_file(bucket, key, dest, Config=self.s3config)
download_file(bucket, key, dest)

@retry(stop=retry_stop, retry=retry_s3_exception, before_sleep=retry_callback)
def exists_impl(self, url):
assert url.startswith("s3://")
bucket, key = url[len("s3://"):].split('/', 1)
# sanity check that the bucket exists and we have access to it
self.s3.head_bucket(Bucket=bucket)
try:
self.s3.head_object(Bucket=bucket, Key=key)
except ClientError as e:
if e.response['Error']['Code'] == '404':
return False
raise e
return True
head_bucket(bucket=bucket)
return head_object(bucket=bucket, key=key)


class LocalFetcher(Fetcher):
Expand Down
6 changes: 3 additions & 3 deletions src/cmd-buildupload
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ CACHE_MAX_AGE_ARTIFACT = 60 * 60 * 24 * 365
# set metadata caching to 5m
CACHE_MAX_AGE_METADATA = 60 * 5
from cosalib.builds import Builds, BUILDFILES
from cosalib.cmdlib import load_json, retry_stop, retry_s3_exception, retry_callback # noqa: E402
from cosalib.cmdlib import load_json, retry_stop, retry_boto_exception, retry_callback # noqa: E402


def main():
Expand Down Expand Up @@ -149,7 +149,7 @@ def s3_upload_build(args, builddir, bucket, prefix):
dry_run=args.dry_run)


@retry(stop=retry_stop, retry=retry_s3_exception, before_sleep=retry_callback)
@retry(stop=retry_stop, retry=retry_boto_exception, before_sleep=retry_callback)
def s3_check_exists(bucket, key):
print(f"Checking if bucket '{bucket}' has key '{key}'")
s3 = boto3.client('s3')
Expand All @@ -162,7 +162,7 @@ def s3_check_exists(bucket, key):
return True


@retry(stop=retry_stop, retry=retry_s3_exception, retry_error_callback=retry_callback)
@retry(stop=retry_stop, retry=retry_boto_exception, retry_error_callback=retry_callback)
def s3_copy(src, bucket, key, max_age, acl, extra_args={}, dry_run=False):
if key.endswith('.json') and 'ContentType' not in extra_args:
extra_args['ContentType'] = 'application/json'
Expand Down
19 changes: 19 additions & 0 deletions src/cosalib/aws.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import boto3
from cosalib.cmdlib import (
retry_stop,
retry_boto_exception,
retry_callback
)
from tenacity import retry


@retry(stop=retry_stop, retry=retry_boto_exception, before_sleep=retry_callback)
def deregister_ami(ami_id, region):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to grow a lot of these, I think we could auto-generate them. But fine as is now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we'll have more than a few here. If that list grows large ore should be updated instead

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, fair enough. And right...this is one of many "mantle vs cosa" issues.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Missing docstring

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we're just wrapping the underlying API call...doesn't seem worth it to copy/paste it or have even more boilerplate docs like "Retry deregister_ami" right?

Feels like we want to generate these.

(On this topic of course, https://www.terraform.io/ is one of several projects implementing a "retry IaaS API calls" pattern)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, these are just simple wrappers, I don't think its worth adding docstrings there

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be more of a personal preference, but having a simple doc with a function tends to be helpful. Keep in mind, this is a nit and isn't required to be updated for merge. I asked @vrutkovs if he's ready for this to merge via chat as I'm happy to hit the button 🙂.

What I hope for is:

"""
Removes an AMI from a specific region.
"""

or even

"""
.. seealso:: :py:func:`ec2.deregister_image`
"""

but your point is well taken @cgwalters.

ec2 = boto3.client('ec2', region_name=region)
ec2.deregister_image(ImageId=ami_id)


@retry(stop=retry_stop, retry=retry_boto_exception, before_sleep=retry_callback)
def delete_snapshot(snap_id, region):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Missing docstring

ec2 = boto3.client('ec2', region_name=region)
ec2.delete_snapshot(SnapshotId=snap_id)
2 changes: 1 addition & 1 deletion src/cosalib/cmdlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from datetime import datetime

retry_stop = (stop_after_delay(10) | stop_after_attempt(5))
retry_s3_exception = (retry_if_exception_type(ConnectionClosedError) |
retry_boto_exception = (retry_if_exception_type(ConnectionClosedError) |
retry_if_exception_type(ConnectTimeoutError) |
retry_if_exception_type(IncompleteReadError) |
retry_if_exception_type(ReadTimeoutError))
Expand Down
59 changes: 59 additions & 0 deletions src/cosalib/s3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import boto3

from botocore.exceptions import ClientError
from cosalib.cmdlib import (
retry_stop,
retry_boto_exception,
retry_callback
)
from tenacity import retry


S3 = boto3.client('s3')
S3CONFIG = boto3.s3.transfer.TransferConfig(
num_download_attempts=5
)


def download_file(bucket, key, dest):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Missing docstring

S3.download_file(bucket, key, dest, Config=S3CONFIG)


@retry(stop=retry_stop, retry=retry_boto_exception, before_sleep=retry_callback)
def head_bucket(bucket):
S3.head_bucket(Bucket=bucket)


@retry(stop=retry_stop, retry=retry_boto_exception, before_sleep=retry_callback)
def head_object(bucket, key):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Missing docstring

try:
S3.head_object(Bucket=bucket, Key=key)
except ClientError as e:
if e.response['Error']['Code'] == '404':
return False
raise e
return True


@retry(stop=retry_stop, retry=retry_boto_exception, before_sleep=retry_callback)
def list_objects(bucket, prefix, delimiter="/", result_key='Contents'):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Missing docstring

kwargs = {
'Bucket': bucket,
'Delimiter': delimiter,
'Prefix': prefix,
}
isTruncated = True
while isTruncated:
batch = S3.list_objects_v2(**kwargs)
yield from batch.get(result_key) or []
kwargs['ContinuationToken'] = batch.get('NextContinuationToken')
isTruncated = batch['IsTruncated']


@retry(stop=retry_stop, retry=retry_boto_exception, before_sleep=retry_callback)
def delete_object(bucket, key):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Missing docstring

sub_objects = list(list_objects(bucket, key))
if sub_objects != []:
print("S3: deleting {sub_objects}")
S3.delete_objects(Bucket=bucket, Delete=sub_objects)
S3.delete_object(Bucket=bucket, Key=key)